feat: improve sync (#429)

This commit is contained in:
hstyi
2025-03-29 19:09:04 +08:00
committed by GitHub
parent ccb2c6daa0
commit 827d814c7b
20 changed files with 335 additions and 121 deletions

View File

@@ -30,6 +30,7 @@ class Database private constructor(private val env: Environment) : Disposable {
private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight" private const val KEYWORD_HIGHLIGHT_STORE = "KeywordHighlight"
private const val MACRO_STORE = "Macro" private const val MACRO_STORE = "Macro"
private const val KEY_PAIR_STORE = "KeyPair" private const val KEY_PAIR_STORE = "KeyPair"
private const val DELETED_DATA_STORE = "DeletedData"
private val log = LoggerFactory.getLogger(Database::class.java) 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) { fun addSnippet(snippet: Snippet) {
var text = ohMyJson.encodeToString(snippet) var text = ohMyJson.encodeToString(snippet)
if (doorman.isWorking()) { 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> { fun getSnippets(): Collection<Snippet> {
val isWorking = doorman.isWorking() val isWorking = doorman.isWorking()

View 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 }
}
}

View File

@@ -214,6 +214,27 @@ data class EncryptedHost(
var updateDate: Long = 0L, 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 @Serializable
data class Host( data class Host(

View File

@@ -16,14 +16,20 @@ class HostManager private constructor() {
*/ */
fun addHost(host: Host) { fun addHost(host: Host) {
assertEventDispatchThread() assertEventDispatchThread()
database.addHost(host)
if (host.deleted) { if (host.deleted) {
hosts.entries.removeIf { it.value.id == host.id || it.value.parentId == host.id } removeHost(host.id)
} else { } else {
database.addHost(host)
hosts[host.id] = 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)
}
/** /**
* 第一次调用从数据库中获取,后续从缓存中获取 * 第一次调用从数据库中获取,后续从缓存中获取
*/ */

View File

@@ -59,11 +59,11 @@ import java.io.File
import java.net.URI import java.net.URI
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.util.* import java.util.*
import java.util.function.Consumer
import javax.swing.* import javax.swing.*
import javax.swing.event.DocumentEvent import javax.swing.event.DocumentEvent
import javax.swing.event.PopupMenuEvent import javax.swing.event.PopupMenuEvent
import javax.swing.event.PopupMenuListener import javax.swing.event.PopupMenuListener
import kotlin.time.Duration.Companion.milliseconds
class SettingsOptionsPane : OptionsPane() { class SettingsOptionsPane : OptionsPane() {
@@ -79,7 +79,6 @@ class SettingsOptionsPane : OptionsPane() {
companion object { companion object {
private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java) private val log = LoggerFactory.getLogger(SettingsOptionsPane::class.java)
private val localShells by lazy { loadShells() } private val localShells by lazy { loadShells() }
var pulled = false
private fun loadShells(): List<String> { private fun loadShells(): List<String> {
val shells = mutableListOf<String>() val shells = mutableListOf<String>()
@@ -568,10 +567,9 @@ class SettingsOptionsPane : OptionsPane() {
val tokenTextField = OutlinePasswordField(255) val tokenTextField = OutlinePasswordField(255)
val gistTextField = OutlineTextField(255) val gistTextField = OutlineTextField(255)
val domainTextField = 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 exportConfigButton = JButton(I18n.getString("termora.settings.sync.export"), Icons.export)
val importConfigButton = JButton(I18n.getString("termora.settings.sync.import"), Icons.import) 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 lastSyncTimeLabel = JLabel()
val sync get() = database.sync val sync get() = database.sync
val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts")) val hostsCheckBox = JCheckBox(I18n.getString("termora.welcome.my-hosts"))
@@ -591,16 +589,8 @@ class SettingsOptionsPane : OptionsPane() {
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
private fun initEvents() { private fun initEvents() {
downloadConfigButton.addActionListener { syncConfigButton.addActionListener {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) { sync() }
pushOrPull(false)
}
}
uploadConfigButton.addActionListener {
GlobalScope.launch(Dispatchers.IO) {
pushOrPull(true)
}
} }
typeComboBox.addItemListener { 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() { private fun refreshButtons() {
sync.rangeKeyPairs = keysCheckBox.isSelected sync.rangeKeyPairs = keysCheckBox.isSelected
sync.rangeHosts = hostsCheckBox.isSelected sync.rangeHosts = hostsCheckBox.isSelected
sync.rangeSnippets = snippetsCheckBox.isSelected sync.rangeSnippets = snippetsCheckBox.isSelected
sync.rangeKeywordHighlights = keywordHighlightsCheckBox.isSelected sync.rangeKeywordHighlights = keywordHighlightsCheckBox.isSelected
downloadConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.isSelected syncConfigButton.isEnabled = keysCheckBox.isSelected || hostsCheckBox.isSelected
|| keywordHighlightsCheckBox.isSelected || keywordHighlightsCheckBox.isSelected
uploadConfigButton.isEnabled = downloadConfigButton.isEnabled exportConfigButton.isEnabled = syncConfigButton.isEnabled
exportConfigButton.isEnabled = downloadConfigButton.isEnabled importConfigButton.isEnabled = syncConfigButton.isEnabled
importConfigButton.isEnabled = downloadConfigButton.isEnabled
} }
private fun export() { private fun export() {
@@ -1022,8 +1036,11 @@ class SettingsOptionsPane : OptionsPane() {
) )
} }
/**
* @return true 同步成功
*/
@Suppress("DuplicatedCode") @Suppress("DuplicatedCode")
private suspend fun pushOrPull(push: Boolean) { private suspend fun pushOrPull(push: Boolean): Boolean {
if (typeComboBox.selectedItem == SyncType.GitLab) { if (typeComboBox.selectedItem == SyncType.GitLab) {
if (domainTextField.text.isBlank()) { if (domainTextField.text.isBlank()) {
@@ -1031,7 +1048,7 @@ class SettingsOptionsPane : OptionsPane() {
domainTextField.outline = "error" domainTextField.outline = "error"
domainTextField.requestFocusInWindow() domainTextField.requestFocusInWindow()
} }
return return false
} }
} }
@@ -1040,7 +1057,7 @@ class SettingsOptionsPane : OptionsPane() {
tokenTextField.outline = "error" tokenTextField.outline = "error"
tokenTextField.requestFocusInWindow() tokenTextField.requestFocusInWindow()
} }
return return false
} }
if (gistTextField.text.isBlank() && !push) { if (gistTextField.text.isBlank() && !push) {
@@ -1048,39 +1065,13 @@ class SettingsOptionsPane : OptionsPane() {
gistTextField.outline = "error" gistTextField.outline = "error"
gistTextField.requestFocusInWindow() gistTextField.requestFocusInWindow()
} }
return return false
}
// 没有拉取过 && 是推送 && 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
}
} }
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
exportConfigButton.isEnabled = false exportConfigButton.isEnabled = false
importConfigButton.isEnabled = false importConfigButton.isEnabled = false
downloadConfigButton.isEnabled = false syncConfigButton.isEnabled = false
uploadConfigButton.isEnabled = false
typeComboBox.isEnabled = false typeComboBox.isEnabled = false
gistTextField.isEnabled = false gistTextField.isEnabled = false
tokenTextField.isEnabled = false tokenTextField.isEnabled = false
@@ -1091,12 +1082,7 @@ class SettingsOptionsPane : OptionsPane() {
hostsCheckBox.isEnabled = false hostsCheckBox.isEnabled = false
snippetsCheckBox.isEnabled = false snippetsCheckBox.isEnabled = false
domainTextField.isEnabled = false domainTextField.isEnabled = false
syncConfigButton.text = "${I18n.getString("termora.settings.sync")}..."
if (push) {
uploadConfigButton.text = "${I18n.getString("termora.settings.sync.push")}..."
} else {
downloadConfigButton.text = "${I18n.getString("termora.settings.sync.pull")}..."
}
} }
val syncConfig = getSyncConfig() val syncConfig = getSyncConfig()
@@ -1113,10 +1099,9 @@ class SettingsOptionsPane : OptionsPane() {
// 恢复状态 // 恢复状态
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
downloadConfigButton.isEnabled = true syncConfigButton.isEnabled = true
exportConfigButton.isEnabled = true exportConfigButton.isEnabled = true
importConfigButton.isEnabled = true importConfigButton.isEnabled = true
uploadConfigButton.isEnabled = true
keysCheckBox.isEnabled = true keysCheckBox.isEnabled = true
hostsCheckBox.isEnabled = true hostsCheckBox.isEnabled = true
snippetsCheckBox.isEnabled = true snippetsCheckBox.isEnabled = true
@@ -1127,11 +1112,7 @@ class SettingsOptionsPane : OptionsPane() {
tokenTextField.isEnabled = true tokenTextField.isEnabled = true
domainTextField.isEnabled = true domainTextField.isEnabled = true
keywordHighlightsCheckBox.isEnabled = true keywordHighlightsCheckBox.isEnabled = true
if (push) { syncConfigButton.text = I18n.getString("termora.settings.sync")
uploadConfigButton.text = I18n.getString("termora.settings.sync.push")
} else {
downloadConfigButton.text = I18n.getString("termora.settings.sync.pull")
}
} }
// 如果失败,提示错误 // 如果失败,提示错误
@@ -1151,10 +1132,8 @@ class SettingsOptionsPane : OptionsPane() {
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
OptionPane.showMessageDialog(owner, message, messageType = JOptionPane.ERROR_MESSAGE) OptionPane.showMessageDialog(owner, message, messageType = JOptionPane.ERROR_MESSAGE)
} }
} else {
// pulled
if (!pulled) pulled = !push
} else {
withContext(Dispatchers.Swing) { withContext(Dispatchers.Swing) {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
sync.lastSyncTime = now sync.lastSyncTime = now
@@ -1163,14 +1142,10 @@ class SettingsOptionsPane : OptionsPane() {
if (push && gistTextField.text.isBlank()) { if (push && gistTextField.text.isBlank()) {
gistTextField.text = syncResult.map { it.config }.getOrDefault(syncConfig).gistId 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 // Sync buttons
.add( .add(
FormBuilder.create() FormBuilder.create()
.layout(FormLayout("pref, 2dlu, pref, 2dlu, pref, 2dlu, pref", "pref")) .layout(FormLayout("pref, 2dlu, pref, 2dlu, pref", "pref"))
.add(uploadConfigButton).xy(1, 1) .add(syncConfigButton).xy(1, 1)
.add(downloadConfigButton).xy(3, 1) .add(exportConfigButton).xy(3, 1)
.add(exportConfigButton).xy(5, 1) .add(importConfigButton).xy(5, 1)
.add(importConfigButton).xy(7, 1)
.build() .build()
).xy(3, rows, "center, fill").apply { rows += step } ).xy(3, rows, "center, fill").apply { rows += step }
.add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step } .add(lastSyncTimeLabel).xy(3, rows, "center, fill").apply { rows += step }

View File

@@ -197,6 +197,7 @@ open class SimpleTree : JXTree() {
} }
override fun importData(support: TransferSupport): Boolean { override fun importData(support: TransferSupport): Boolean {
if (!support.isDrop) return false
val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false val dropLocation = support.dropLocation as? JTree.DropLocation ?: return false
val node = dropLocation.path.lastPathComponent as? SimpleTreeNode<*> ?: return false val node = dropLocation.path.lastPathComponent as? SimpleTreeNode<*> ?: return false
val nodes = (support.transferable.getTransferData(MoveNodeTransferable.dataFlavor) as? List<*>) 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 onRenamed(node: SimpleTreeNode<*>, text: String) {}
protected open fun refreshNode(node: SimpleTreeNode<*>) { open fun refreshNode(node: SimpleTreeNode<*> = model.root) {
val state = TreeUtils.saveExpansionState(tree) val state = TreeUtils.saveExpansionState(tree)
val rows = selectionRows val rows = selectionRows

View File

@@ -62,5 +62,10 @@ data class KeywordHighlight(
/** /**
* 排序 * 排序
*/ */
val sort: Long = System.currentTimeMillis() val sort: Long = System.currentTimeMillis(),
/**
* 更新时间
*/
val updateDate: Long = System.currentTimeMillis(),
) )

View File

@@ -2,6 +2,7 @@ package app.termora.highlight
import app.termora.ApplicationScope import app.termora.ApplicationScope
import app.termora.Database import app.termora.Database
import app.termora.DeleteDataManager
import app.termora.TerminalPanelFactory import app.termora.TerminalPanelFactory
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -38,6 +39,7 @@ class KeywordHighlightManager private constructor() {
database.removeKeywordHighlight(id) database.removeKeywordHighlight(id)
keywordHighlights.remove(id) keywordHighlights.remove(id)
TerminalPanelFactory.getInstance().repaintAll() TerminalPanelFactory.getInstance().repaintAll()
DeleteDataManager.getInstance().removeKeywordHighlight(id)
if (log.isDebugEnabled) { if (log.isDebugEnabled) {
log.debug("Keyword highlighter removed. {}", id) log.debug("Keyword highlighter removed. {}", id)

View File

@@ -89,6 +89,7 @@ class KeymapManager private constructor() : Disposable {
fun removeKeymap(name: String) { fun removeKeymap(name: String) {
keymaps.remove(name) keymaps.remove(name)
database.removeKeymap(name) database.removeKeymap(name)
DeleteDataManager.getInstance().removeKeymap(name)
} }
private inner class KeymapKeyEventDispatcher : KeyEventDispatcher { private inner class KeymapKeyEventDispatcher : KeyEventDispatcher {

View File

@@ -2,6 +2,7 @@ package app.termora.keymgr
import app.termora.ApplicationScope import app.termora.ApplicationScope
import app.termora.Database import app.termora.Database
import app.termora.DeleteDataManager
class KeyManager private constructor() { class KeyManager private constructor() {
companion object { companion object {
@@ -29,6 +30,7 @@ class KeyManager private constructor() {
fun removeOhKeyPair(id: String) { fun removeOhKeyPair(id: String) {
keyPairs.removeIf { it.id == id } keyPairs.removeIf { it.id == id }
database.removeKeyPair(id) database.removeKeyPair(id)
DeleteDataManager.getInstance().removeKeyPair(id)
} }
fun getOhKeyPairs(): List<OhKeyPair> { fun getOhKeyPairs(): List<OhKeyPair> {
@@ -39,9 +41,4 @@ class KeyManager private constructor() {
return keyPairs.findLast { it.id == id } return keyPairs.findLast { it.id == id }
} }
fun removeAll() {
keyPairs.clear()
database.removeAllKeyPair()
}
} }

View File

@@ -15,6 +15,7 @@ data class OhKeyPair(
val remark: String, val remark: String,
val length: Int, val length: Int,
val sort: Long, val sort: Long,
val updateDate: Long = System.currentTimeMillis(),
) { ) {
companion object { companion object {
val empty = OhKeyPair(String(), String(), String(), String(), String(), String(), 0, 0) val empty = OhKeyPair(String(), String(), String(), String(), String(), String(), 0, 0)

View File

@@ -19,6 +19,11 @@ data class Macro(
* 越大越靠前 * 越大越靠前
*/ */
val sort: Long = System.currentTimeMillis(), val sort: Long = System.currentTimeMillis(),
/**
* 更新时间
*/
val updateDate: Long = System.currentTimeMillis(),
) { ) {
val macroByteArray by lazy { macro.decodeBase64() } val macroByteArray by lazy { macro.decodeBase64() }
} }

View File

@@ -2,6 +2,7 @@ package app.termora.macro
import app.termora.ApplicationScope import app.termora.ApplicationScope
import app.termora.Database import app.termora.Database
import app.termora.DeleteDataManager
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
/** /**
@@ -38,6 +39,7 @@ class MacroManager private constructor() {
fun removeMacro(id: String) { fun removeMacro(id: String) {
database.removeMacro(id) database.removeMacro(id)
macros.remove(id) macros.remove(id)
DeleteDataManager.getInstance().removeMacro(id)
if (log.isDebugEnabled) { if (log.isDebugEnabled) {
log.debug("Removed macro $id") log.debug("Removed macro $id")

View File

@@ -2,6 +2,7 @@ package app.termora.snippet
import app.termora.ApplicationScope import app.termora.ApplicationScope
import app.termora.Database import app.termora.Database
import app.termora.DeleteDataManager
import app.termora.assertEventDispatchThread import app.termora.assertEventDispatchThread
@@ -20,14 +21,20 @@ class SnippetManager private constructor() {
*/ */
fun addSnippet(snippet: Snippet) { fun addSnippet(snippet: Snippet) {
assertEventDispatchThread() assertEventDispatchThread()
database.addSnippet(snippet)
if (snippet.deleted) { if (snippet.deleted) {
snippets.entries.removeIf { it.value.id == snippet.id || it.value.parentId == snippet.id } removeSnippet(snippet.id)
} else { } else {
database.addSnippet(snippet)
snippets[snippet.id] = 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)
}
/** /**
* 第一次调用从数据库中获取,后续从缓存中获取 * 第一次调用从数据库中获取,后续从缓存中获取
*/ */

View File

@@ -1,6 +1,7 @@
package app.termora.sync package app.termora.sync
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.DeletedData
import app.termora.ResponseException import app.termora.ResponseException
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
@@ -26,46 +27,51 @@ abstract class GitSyncer : SafetySyncer() {
} }
val gistResponse = parsePullResponse(response, config) 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 // decode hosts
if (config.ranges.contains(SyncRange.Hosts)) { if (config.ranges.contains(SyncRange.Hosts)) {
gistResponse.gists.findLast { it.filename == "Hosts" }?.let { gistResponse.gists.findLast { it.filename == "Hosts" }?.let {
decodeHosts(it.content, config) decodeHosts(it.content, deletedData.filter { e -> e.type == "Host" }, config)
} }
} }
// decode keys // decode keys
if (config.ranges.contains(SyncRange.KeyPairs)) { if (config.ranges.contains(SyncRange.KeyPairs)) {
gistResponse.gists.findLast { it.filename == "KeyPairs" }?.let { gistResponse.gists.findLast { it.filename == "KeyPairs" }?.let {
decodeKeys(it.content, config) decodeKeys(it.content, deletedData.filter { e -> e.type == "KeyPair" }, config)
} }
} }
// decode keyword highlights // decode keyword highlights
if (config.ranges.contains(SyncRange.KeywordHighlights)) { if (config.ranges.contains(SyncRange.KeywordHighlights)) {
gistResponse.gists.findLast { it.filename == "KeywordHighlights" }?.let { gistResponse.gists.findLast { it.filename == "KeywordHighlights" }?.let {
decodeKeywordHighlights(it.content, config) decodeKeywordHighlights(it.content, deletedData.filter { e -> e.type == "KeywordHighlight" }, config)
} }
} }
// decode macros // decode macros
if (config.ranges.contains(SyncRange.Macros)) { if (config.ranges.contains(SyncRange.Macros)) {
gistResponse.gists.findLast { it.filename == "Macros" }?.let { gistResponse.gists.findLast { it.filename == "Macros" }?.let {
decodeMacros(it.content, config) decodeMacros(it.content, deletedData.filter { e -> e.type == "Macro" }, config)
} }
} }
// decode keymaps // decode keymaps
if (config.ranges.contains(SyncRange.Macros)) { if (config.ranges.contains(SyncRange.Macros)) {
gistResponse.gists.findLast { it.filename == "Keymaps" }?.let { gistResponse.gists.findLast { it.filename == "Keymaps" }?.let {
decodeKeymaps(it.content, config) decodeKeymaps(it.content, deletedData.filter { e -> e.type == "Keymap" }, config)
} }
} }
// decode Snippets // decode Snippets
if (config.ranges.contains(SyncRange.Snippets)) { if (config.ranges.contains(SyncRange.Snippets)) {
gistResponse.gists.findLast { it.filename == "Snippets" }?.let { 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 { override fun push(config: SyncConfig): GistResponse {
if (log.isInfoEnabled) {
log.info("Type: ${config.type} , Gist: ${config.gistId} Push...")
}
val gistFiles = mutableListOf<GistFile>() val gistFiles = mutableListOf<GistFile>()
// aes key // aes key
val key = ArrayUtils.subarray(config.token.padEnd(16, '0').toByteArray(), 0, 16) 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") 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() 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 { open fun parsePullResponse(response: Response, config: SyncConfig): GistResponse {

View File

@@ -34,8 +34,9 @@ abstract class SafetySyncer : Syncer {
protected val macroManager get() = MacroManager.getInstance() protected val macroManager get() = MacroManager.getInstance()
protected val keymapManager get() = KeymapManager.getInstance() protected val keymapManager get() = KeymapManager.getInstance()
protected val snippetManager get() = SnippetManager.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 // aes key
val key = getKey(config) val key = getKey(config)
val encryptedHosts = ohMyJson.decodeFromString<List<EncryptedHost>>(text) val encryptedHosts = ohMyJson.decodeFromString<List<EncryptedHost>>(text)
@@ -44,9 +45,9 @@ abstract class SafetySyncer : Syncer {
for (encryptedHost in encryptedHosts) { for (encryptedHost in encryptedHosts) {
val oldHost = hosts[encryptedHost.id] val oldHost = hosts[encryptedHost.id]
// 如果一样,则无需配置 // 如果本地的修改时间大于云端时间,那么跳过
if (oldHost != null) { if (oldHost != null) {
if (oldHost.updateDate == encryptedHost.updateDate) { if (oldHost.updateDate >= encryptedHost.updateDate) {
continue continue
} }
} }
@@ -83,7 +84,6 @@ abstract class SafetySyncer : Syncer {
creatorId = encryptedHost.creatorId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(), creatorId = encryptedHost.creatorId.decodeBase64().aesCBCDecrypt(key, iv).decodeToString(),
createDate = encryptedHost.createDate, createDate = encryptedHost.createDate,
updateDate = encryptedHost.updateDate, updateDate = encryptedHost.updateDate,
deleted = encryptedHost.deleted
) )
SwingUtilities.invokeLater { hostManager.addHost(host) } SwingUtilities.invokeLater { hostManager.addHost(host) }
} catch (e: Exception) { } 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) { if (log.isDebugEnabled) {
log.debug("Decode hosts: {}", text) log.debug("Decode hosts: {}", text)
@@ -120,7 +126,6 @@ abstract class SafetySyncer : Syncer {
encryptedHost.tunnelings = encryptedHost.tunnelings =
ohMyJson.encodeToString(host.tunnelings).aesCBCEncrypt(key, iv).encodeBase64String() ohMyJson.encodeToString(host.tunnelings).aesCBCEncrypt(key, iv).encodeBase64String()
encryptedHost.sort = host.sort encryptedHost.sort = host.sort
encryptedHost.deleted = host.deleted
encryptedHost.parentId = host.parentId.aesCBCEncrypt(key, iv).encodeBase64String() encryptedHost.parentId = host.parentId.aesCBCEncrypt(key, iv).encodeBase64String()
encryptedHost.ownerId = host.ownerId.aesCBCEncrypt(key, iv).encodeBase64String() encryptedHost.ownerId = host.ownerId.aesCBCEncrypt(key, iv).encodeBase64String()
encryptedHost.creatorId = host.creatorId.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 // aes key
val key = getKey(config) val key = getKey(config)
val encryptedSnippets = ohMyJson.decodeFromString<List<Snippet>>(text) val encryptedSnippets = ohMyJson.decodeFromString<List<Snippet>>(text)
@@ -144,7 +160,7 @@ abstract class SafetySyncer : Syncer {
// 如果一样,则无需配置 // 如果一样,则无需配置
if (oldHost != null) { if (oldHost != null) {
if (oldHost.updateDate == encryptedSnippet.updateDate) { if (oldHost.updateDate >= encryptedSnippet.updateDate) {
continue 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) { 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 // aes key
val key = getKey(config) val key = getKey(config)
val encryptedKeys = ohMyJson.decodeFromString<List<OhKeyPair>>(text) val encryptedKeys = ohMyJson.decodeFromString<List<OhKeyPair>>(text)
val keys = keyManager.getOhKeyPairs().associateBy { it.id }
for (encryptedKey in encryptedKeys) { for (encryptedKey in encryptedKeys) {
val k = keys[encryptedKey.id]
if (k != null) {
if (k.updateDate > encryptedKey.updateDate) {
continue
}
}
try { try {
// aes iv // aes iv
val iv = getIv(encryptedKey.id) 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) { if (log.isDebugEnabled) {
log.debug("Decode keys: {}", text) log.debug("Decode keys: {}", text)
} }
@@ -240,12 +277,20 @@ abstract class SafetySyncer : Syncer {
return ohMyJson.encodeToString(encryptedKeys) return ohMyJson.encodeToString(encryptedKeys)
} }
protected fun decodeKeywordHighlights(text: String, config: SyncConfig) { protected fun decodeKeywordHighlights(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
// aes key // aes key
val key = getKey(config) val key = getKey(config)
val encryptedKeywordHighlights = ohMyJson.decodeFromString<List<KeywordHighlight>>(text) val encryptedKeywordHighlights = ohMyJson.decodeFromString<List<KeywordHighlight>>(text)
val keywordHighlights = keywordHighlightManager.getKeywordHighlights().associateBy { it.id }
for (e in encryptedKeywordHighlights) { for (e in encryptedKeywordHighlights) {
val keywordHighlight = keywordHighlights[e.id]
if (keywordHighlight != null) {
if (keywordHighlight.updateDate >= e.updateDate) {
continue
}
}
try { try {
// aes iv // aes iv
val iv = getIv(e.id) 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) { if (log.isDebugEnabled) {
log.debug("Decode KeywordHighlight: {}", text) log.debug("Decode KeywordHighlight: {}", text)
} }
@@ -281,12 +333,19 @@ abstract class SafetySyncer : Syncer {
return ohMyJson.encodeToString(keywordHighlights) return ohMyJson.encodeToString(keywordHighlights)
} }
protected fun decodeMacros(text: String, config: SyncConfig) { protected fun decodeMacros(text: String, deletedData: List<DeletedData>, config: SyncConfig) {
// aes key // aes key
val key = getKey(config) val key = getKey(config)
val encryptedMacros = ohMyJson.decodeFromString<List<Macro>>(text) val encryptedMacros = ohMyJson.decodeFromString<List<Macro>>(text)
val macros = macroManager.getMacros().associateBy { it.id }
for (e in encryptedMacros) { for (e in encryptedMacros) {
val macro = macros[e.id]
if (macro != null) {
if (macro.updateDate >= e.updateDate) {
continue
}
}
try { try {
// aes iv // aes iv
val iv = getIv(e.id) 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) { if (log.isDebugEnabled) {
log.debug("Decode Macros: {}", text) log.debug("Decode Macros: {}", text)
} }
@@ -322,12 +388,19 @@ abstract class SafetySyncer : Syncer {
return ohMyJson.encodeToString(macros) 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) }) { for (keymap in ohMyJson.decodeFromString<List<JsonObject>>(text).mapNotNull { Keymap.fromJSON(it) }) {
keymapManager.addKeymap(keymap) keymapManager.addKeymap(keymap)
} }
SwingUtilities.invokeLater {
deletedData.forEach {
keymapManager.removeKeymap(it.id)
deleteDataManager.removeKeymap(it.id, it.deleteDate)
}
}
if (log.isDebugEnabled) { if (log.isDebugEnabled) {
log.debug("Decode Keymaps: {}", text) log.debug("Decode Keymaps: {}", text)
} }

View File

@@ -2,6 +2,7 @@ package app.termora.sync
import app.termora.Application.ohMyJson import app.termora.Application.ohMyJson
import app.termora.ApplicationScope import app.termora.ApplicationScope
import app.termora.DeletedData
import app.termora.PBKDF2 import app.termora.PBKDF2
import app.termora.ResponseException import app.termora.ResponseException
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
@@ -27,6 +28,9 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
override fun pull(config: SyncConfig): GistResponse { override fun pull(config: SyncConfig): GistResponse {
val response = httpClient.newCall(newRequestBuilder(config).get().build()).execute() val response = httpClient.newCall(newRequestBuilder(config).get().build()).execute()
if (!response.isSuccessful) { if (!response.isSuccessful) {
if (response.code == 404) {
return GistResponse(config, emptyList())
}
throw ResponseException(response.code, response) throw ResponseException(response.code, response)
} }
@@ -34,46 +38,48 @@ class WebDAVSyncer private constructor() : SafetySyncer() {
?: throw ResponseException(response.code, response) ?: throw ResponseException(response.code, response)
val json = ohMyJson.decodeFromString<JsonObject>(text) val json = ohMyJson.decodeFromString<JsonObject>(text)
val deletedData = mutableListOf<DeletedData>()
json["DeletedData"]?.jsonPrimitive?.content?.let { deletedData.addAll(decodeDeletedData(it, config)) }
// decode hosts // decode hosts
if (config.ranges.contains(SyncRange.Hosts)) { if (config.ranges.contains(SyncRange.Hosts)) {
json["Hosts"]?.jsonPrimitive?.content?.let { json["Hosts"]?.jsonPrimitive?.content?.let {
decodeHosts(it, config) decodeHosts(it, deletedData.filter { e -> e.type == "Host" }, config)
} }
} }
// decode KeyPairs // decode KeyPairs
if (config.ranges.contains(SyncRange.KeyPairs)) { if (config.ranges.contains(SyncRange.KeyPairs)) {
json["KeyPairs"]?.jsonPrimitive?.content?.let { json["KeyPairs"]?.jsonPrimitive?.content?.let {
decodeKeys(it, config) decodeKeys(it, deletedData.filter { e -> e.type == "KeyPair" }, config)
} }
} }
// decode Highlights // decode Highlights
if (config.ranges.contains(SyncRange.KeywordHighlights)) { if (config.ranges.contains(SyncRange.KeywordHighlights)) {
json["KeywordHighlights"]?.jsonPrimitive?.content?.let { json["KeywordHighlights"]?.jsonPrimitive?.content?.let {
decodeKeywordHighlights(it, config) decodeKeywordHighlights(it, deletedData.filter { e -> e.type == "KeywordHighlight" }, config)
} }
} }
// decode Macros // decode Macros
if (config.ranges.contains(SyncRange.Macros)) { if (config.ranges.contains(SyncRange.Macros)) {
json["Macros"]?.jsonPrimitive?.content?.let { json["Macros"]?.jsonPrimitive?.content?.let {
decodeMacros(it, config) decodeMacros(it, deletedData.filter { e -> e.type == "Macro" }, config)
} }
} }
// decode Keymaps // decode Keymaps
if (config.ranges.contains(SyncRange.Keymap)) { if (config.ranges.contains(SyncRange.Keymap)) {
json["Keymaps"]?.jsonPrimitive?.content?.let { json["Keymaps"]?.jsonPrimitive?.content?.let {
decodeKeymaps(it, config) decodeKeymaps(it, deletedData.filter { e -> e.type == "Keymap" }, config)
} }
} }
// decode Snippets // decode Snippets
if (config.ranges.contains(SyncRange.Snippets)) { if (config.ranges.contains(SyncRange.Snippets)) {
json["Snippets"]?.jsonPrimitive?.content?.let { 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) put("Keymaps", keymapsContent)
} }
// deletedData
val deletedData = encodeDeletedData(config)
if (log.isDebugEnabled) {
log.debug("Push DeletedData: {}", deletedData)
}
put("DeletedData", deletedData)
} }
val response = httpClient.newCall( val response = httpClient.newCall(

View File

@@ -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.terminal.auto-close-tab-description=Automatically close the tab when the terminal is disconnected normally
termora.settings.sync=Sync 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.done=Synchronized data successfully
termora.settings.sync.export=${termora.keymgr.export} termora.settings.sync.export=${termora.keymgr.export}
termora.settings.sync.import=${termora.keymgr.import} termora.settings.sync.import=${termora.keymgr.import}

View File

@@ -82,9 +82,6 @@ termora.settings.terminal.auto-close-tab-description=当终端正常断开连接
termora.settings.sync=同步 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-done=导出成功
termora.settings.sync.export-encrypt=输入密码加密文件 (可选) termora.settings.sync.export-encrypt=输入密码加密文件 (可选)
termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹? termora.settings.sync.export-done-open-folder=导出成功,是否需要打开所在文件夹?

View File

@@ -93,9 +93,6 @@ termora.settings.terminal.auto-close-tab=自動關閉標籤
termora.settings.terminal.auto-close-tab-description=當終端正常斷開連線時自動關閉標籤頁 termora.settings.terminal.auto-close-tab-description=當終端正常斷開連線時自動關閉標籤頁
termora.settings.sync=同步 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-done=匯出成功
termora.settings.sync.export-encrypt=輸入密碼加密檔案 (可選) termora.settings.sync.export-encrypt=輸入密碼加密檔案 (可選)
termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾? termora.settings.sync.export-done-open-folder=匯出成功,是否需要打開所在資料夾?