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 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()

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,
)
/**
* 被删除的数据
*/
@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(

View File

@@ -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)
}
/**
* 第一次调用从数据库中获取,后续从缓存中获取
*/

View File

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

View File

@@ -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

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.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)

View File

@@ -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 {

View File

@@ -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()
}
}

View File

@@ -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)

View File

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

View File

@@ -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")

View File

@@ -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)
}
/**
* 第一次调用从数据库中获取,后续从缓存中获取
*/

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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(

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

View File

@@ -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=导出成功,是否需要打开所在文件夹?

View File

@@ -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=匯出成功,是否需要打開所在資料夾?