diff --git a/src/main/kotlin/app/termora/Database.kt b/src/main/kotlin/app/termora/Database.kt index aac7653..a099fc5 100644 --- a/src/main/kotlin/app/termora/Database.kt +++ b/src/main/kotlin/app/termora/Database.kt @@ -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 { + return env.computeInTransaction { tx -> + openCursor(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 { val isWorking = doorman.isWorking() diff --git a/src/main/kotlin/app/termora/DeleteDataManager.kt b/src/main/kotlin/app/termora/DeleteDataManager.kt new file mode 100644 index 0000000..e5a28a1 --- /dev/null +++ b/src/main/kotlin/app/termora/DeleteDataManager.kt @@ -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() + 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 { + if (data.isEmpty()) { + data.putAll(database.getDeletedData().associateBy { it.id }) + } + return data.values.sortedBy { it.deleteDate } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/termora/Host.kt b/src/main/kotlin/app/termora/Host.kt index a6f5fb1..a258c32 100644 --- a/src/main/kotlin/app/termora/Host.kt +++ b/src/main/kotlin/app/termora/Host.kt @@ -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( diff --git a/src/main/kotlin/app/termora/HostManager.kt b/src/main/kotlin/app/termora/HostManager.kt index 8fb2ab1..14f6e75 100644 --- a/src/main/kotlin/app/termora/HostManager.kt +++ b/src/main/kotlin/app/termora/HostManager.kt @@ -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) + } + /** * 第一次调用从数据库中获取,后续从缓存中获取 */ diff --git a/src/main/kotlin/app/termora/SettingsOptionsPane.kt b/src/main/kotlin/app/termora/SettingsOptionsPane.kt index b436136..f1b86d7 100644 --- a/src/main/kotlin/app/termora/SettingsOptionsPane.kt +++ b/src/main/kotlin/app/termora/SettingsOptionsPane.kt @@ -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 { val shells = mutableListOf() @@ -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) { + 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 } diff --git a/src/main/kotlin/app/termora/SimpleTree.kt b/src/main/kotlin/app/termora/SimpleTree.kt index 2d5c709..c3388f4 100644 --- a/src/main/kotlin/app/termora/SimpleTree.kt +++ b/src/main/kotlin/app/termora/SimpleTree.kt @@ -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 diff --git a/src/main/kotlin/app/termora/highlight/KeywordHighlight.kt b/src/main/kotlin/app/termora/highlight/KeywordHighlight.kt index 9d7ec40..f7c7b78 100644 --- a/src/main/kotlin/app/termora/highlight/KeywordHighlight.kt +++ b/src/main/kotlin/app/termora/highlight/KeywordHighlight.kt @@ -62,5 +62,10 @@ data class KeywordHighlight( /** * 排序 */ - val sort: Long = System.currentTimeMillis() + val sort: Long = System.currentTimeMillis(), + + /** + * 更新时间 + */ + val updateDate: Long = System.currentTimeMillis(), ) \ No newline at end of file diff --git a/src/main/kotlin/app/termora/highlight/KeywordHighlightManager.kt b/src/main/kotlin/app/termora/highlight/KeywordHighlightManager.kt index 9866e96..3b56dad 100644 --- a/src/main/kotlin/app/termora/highlight/KeywordHighlightManager.kt +++ b/src/main/kotlin/app/termora/highlight/KeywordHighlightManager.kt @@ -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) diff --git a/src/main/kotlin/app/termora/keymap/KeymapManager.kt b/src/main/kotlin/app/termora/keymap/KeymapManager.kt index 2656137..6d7c9e4 100644 --- a/src/main/kotlin/app/termora/keymap/KeymapManager.kt +++ b/src/main/kotlin/app/termora/keymap/KeymapManager.kt @@ -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 { diff --git a/src/main/kotlin/app/termora/keymgr/KeyManager.kt b/src/main/kotlin/app/termora/keymgr/KeyManager.kt index ed8ebc2..b4dbd89 100644 --- a/src/main/kotlin/app/termora/keymgr/KeyManager.kt +++ b/src/main/kotlin/app/termora/keymgr/KeyManager.kt @@ -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 { @@ -39,9 +41,4 @@ class KeyManager private constructor() { return keyPairs.findLast { it.id == id } } - fun removeAll() { - keyPairs.clear() - database.removeAllKeyPair() - } - } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/keymgr/OhKeyPair.kt b/src/main/kotlin/app/termora/keymgr/OhKeyPair.kt index f36ddf7..8007b93 100644 --- a/src/main/kotlin/app/termora/keymgr/OhKeyPair.kt +++ b/src/main/kotlin/app/termora/keymgr/OhKeyPair.kt @@ -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) diff --git a/src/main/kotlin/app/termora/macro/Macro.kt b/src/main/kotlin/app/termora/macro/Macro.kt index e128bf0..af7bca0 100644 --- a/src/main/kotlin/app/termora/macro/Macro.kt +++ b/src/main/kotlin/app/termora/macro/Macro.kt @@ -19,6 +19,11 @@ data class Macro( * 越大越靠前 */ val sort: Long = System.currentTimeMillis(), + + /** + * 更新时间 + */ + val updateDate: Long = System.currentTimeMillis(), ) { val macroByteArray by lazy { macro.decodeBase64() } } \ No newline at end of file diff --git a/src/main/kotlin/app/termora/macro/MacroManager.kt b/src/main/kotlin/app/termora/macro/MacroManager.kt index 723405e..de54939 100644 --- a/src/main/kotlin/app/termora/macro/MacroManager.kt +++ b/src/main/kotlin/app/termora/macro/MacroManager.kt @@ -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") diff --git a/src/main/kotlin/app/termora/snippet/SnippetManager.kt b/src/main/kotlin/app/termora/snippet/SnippetManager.kt index 2b6188f..14d6821 100644 --- a/src/main/kotlin/app/termora/snippet/SnippetManager.kt +++ b/src/main/kotlin/app/termora/snippet/SnippetManager.kt @@ -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) + } + /** * 第一次调用从数据库中获取,后续从缓存中获取 */ diff --git a/src/main/kotlin/app/termora/sync/GitSyncer.kt b/src/main/kotlin/app/termora/sync/GitSyncer.kt index 55d979b..b43c68c 100644 --- a/src/main/kotlin/app/termora/sync/GitSyncer.kt +++ b/src/main/kotlin/app/termora/sync/GitSyncer.kt @@ -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 + 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() // 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 { diff --git a/src/main/kotlin/app/termora/sync/SafetySyncer.kt b/src/main/kotlin/app/termora/sync/SafetySyncer.kt index 154cf4f..ad1f45a 100644 --- a/src/main/kotlin/app/termora/sync/SafetySyncer.kt +++ b/src/main/kotlin/app/termora/sync/SafetySyncer.kt @@ -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, config: SyncConfig) { // aes key val key = getKey(config) val encryptedHosts = ohMyJson.decodeFromString>(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 { + val deletedData = ohMyJson.decodeFromString>(text).toMutableList() + // 和本地融合 + deletedData.addAll(deleteDataManager.getDeletedData()) + return deletedData + } + + protected fun decodeSnippets(text: String, deletedData: List, config: SyncConfig) { // aes key val key = getKey(config) val encryptedSnippets = ohMyJson.decodeFromString>(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, config: SyncConfig) { // aes key val key = getKey(config) val encryptedKeys = ohMyJson.decodeFromString>(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, config: SyncConfig) { // aes key val key = getKey(config) val encryptedKeywordHighlights = ohMyJson.decodeFromString>(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, config: SyncConfig) { // aes key val key = getKey(config) val encryptedMacros = ohMyJson.decodeFromString>(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, config: SyncConfig) { for (keymap in ohMyJson.decodeFromString>(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) } diff --git a/src/main/kotlin/app/termora/sync/WebDAVSyncer.kt b/src/main/kotlin/app/termora/sync/WebDAVSyncer.kt index 0f3e672..36fd361 100644 --- a/src/main/kotlin/app/termora/sync/WebDAVSyncer.kt +++ b/src/main/kotlin/app/termora/sync/WebDAVSyncer.kt @@ -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(text) + val deletedData = mutableListOf() + 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( diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 84ea044..5ff4757 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -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} diff --git a/src/main/resources/i18n/messages_zh_CN.properties b/src/main/resources/i18n/messages_zh_CN.properties index 3f42713..53194c2 100644 --- a/src/main/resources/i18n/messages_zh_CN.properties +++ b/src/main/resources/i18n/messages_zh_CN.properties @@ -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=导出成功,是否需要打开所在文件夹? diff --git a/src/main/resources/i18n/messages_zh_TW.properties b/src/main/resources/i18n/messages_zh_TW.properties index fadf22a..6e939fd 100644 --- a/src/main/resources/i18n/messages_zh_TW.properties +++ b/src/main/resources/i18n/messages_zh_TW.properties @@ -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=匯出成功,是否需要打開所在資料夾?