chore: improve team sync

This commit is contained in:
hstyi
2025-07-02 16:49:50 +08:00
committed by hstyi
parent 9916edbd13
commit 168c4c5c64
34 changed files with 304 additions and 178 deletions

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.cos
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class COSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
return COSProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return COSProtocolHostPanel()
}
}

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.obs
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class OBSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
return OBSProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return OBSProtocolHostPanel()
}
}

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.oss
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class OSSProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
return OSSProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return OSSProtocolHostPanel()
}
}

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.s3
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class S3ProtocolHostPanelExtension private constructor() : ProtocolHostPanelExte
return S3ProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return S3ProtocolHostPanel()
}
}

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.smb
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class SMBProtocolHostPanelExtension private constructor() : ProtocolHostPanelExt
return SMBProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return SMBProtocolHostPanel()
}
}

View File

@@ -1,5 +1,6 @@
package app.termora.plugins.webdav
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -13,7 +14,7 @@ class WebDAVProtocolHostPanelExtension private constructor() : ProtocolHostPanel
return WebDAVProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return WebDAVProtocolHostPanel()
}
}

View File

@@ -1,5 +1,6 @@
package app.termora
import app.termora.account.AccountOwner
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.protocol.*
@@ -18,7 +19,11 @@ import java.awt.Dimension
import java.awt.Window
import javax.swing.*
class NewHostDialogV2(owner: Window, private val editHost: Host? = null) : DialogWrapper(owner) {
class NewHostDialogV2(
owner: Window,
private val editHost: Host? = null,
private val accountOwner: AccountOwner,
) : DialogWrapper(owner) {
private object Current {
var card: ProtocolHostPanel? = null
@@ -65,11 +70,11 @@ class NewHostDialogV2(owner: Window, private val editHost: Host? = null) : Dialo
toolbar.add(Box.createHorizontalGlue())
val extensions = ProtocolHostPanelExtension.extensions
.filter { it.canCreateProtocolHostPanel() }
.filter { it.canCreateProtocolHostPanel(accountOwner) }
for ((index, extension) in extensions.withIndex()) {
val protocol = extension.getProtocolProvider().getProtocol()
val icon = ScaleIcon(extension.getProtocolProvider().getIcon(), 22)
val hostPanel = extension.createProtocolHostPanel()
val hostPanel = extension.createProtocolHostPanel(accountOwner)
val button = JToggleButton(protocol, icon).apply { buttonGroup.add(this) }
button.setVerticalTextPosition(SwingConstants.BOTTOM)
button.setHorizontalTextPosition(SwingConstants.CENTER)

View File

@@ -33,7 +33,7 @@ open class Scope(
return get(clazz)
}
synchronized(clazz) {
synchronized(this) {
if (beans.containsKey(clazz)) {
return get(clazz)
}

View File

@@ -1,6 +1,7 @@
package app.termora
import app.termora.account.AccountManager
import app.termora.actions.*
import app.termora.database.DatabaseChangedExtension
import app.termora.database.DatabaseManager
@@ -244,10 +245,15 @@ class TerminalTabbed(
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
edit.addActionListener(object : AnAction() {
private val hostManager get() = HostManager.getInstance()
private val accountManager get() = AccountManager.getInstance()
override fun actionPerformed(evt: AnActionEvent) {
if (tab is HostTerminalTab) {
val host = hostManager.getHost(tab.host.id) ?: return
val dialog = NewHostDialogV2(evt.window, host)
val dialog = NewHostDialogV2(
evt.window, host,
accountManager.getOwners().first { it.id == host.ownerId },
)
dialog.setLocationRelativeTo(evt.window)
dialog.isVisible = true

View File

@@ -183,8 +183,8 @@ object AccountHttp {
if (isRefreshing.compareAndSet(false, true)) {
try {
// 刷新 token
accountManager.refreshToken()
// 刷新 token 和用户
accountManager.refresh()
} finally {
lock.withLock {
isRefreshing.set(false)

View File

@@ -14,12 +14,15 @@ import okhttp3.RequestBody.Companion.toRequestBody
import org.apache.commons.codec.binary.Base64
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import java.security.PrivateKey
import java.security.PublicKey
import javax.swing.SwingUtilities
class AccountManager private constructor() : ApplicationRunnerExtension {
companion object {
private val log = LoggerFactory.getLogger(AccountManager::class.java)
fun getInstance(): AccountManager {
return ApplicationScope.forApplicationScope()
.getOrCreate(AccountManager::class) { AccountManager() }
@@ -30,6 +33,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
}
}
private val serverManager get() = ServerManager.getInstance()
private var account = locally()
private val accountProperties get() = AccountProperties.getInstance()
@@ -48,10 +52,14 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
fun getAccessToken() = account.accessToken
fun getRefreshToken() = account.refreshToken
fun getOwnerIds() = account.teams.map { it.id }.toMutableList().apply { add(getAccountId()) }.toSet()
fun getOwners() =
account.teams.map { AccountOwner(it.id, it.name, OwnerType.Team) }
.toMutableList().apply { AccountOwner(getAccountId(), getEmail(), OwnerType.User) }
.toSet()
fun getOwners(): Set<AccountOwner> {
val owners = mutableSetOf<AccountOwner>()
owners.add(AccountOwner(getAccountId(), getEmail(), OwnerType.User))
for (team in getTeams()) {
owners.add(AccountOwner(team.id, team.name, OwnerType.Team))
}
return owners
}
fun isFreePlan(): Boolean {
return isLocally() || getSubscription().plan == SubscriptionPlan.Free
@@ -126,6 +134,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
* 设置账户信息,可以多次调用,每次修改用户信息都要通过这个方法
*/
internal fun login(account: Account) {
synchronized(this) {
val oldAccount = this.account
@@ -158,6 +167,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
// 通知变化
notifyAccountChanged(oldAccount, account)
}
}
private fun notifyAccountChanged(oldAccount: Account, newAccount: Account) {
if (SwingUtilities.isEventDispatchThread()) {
@@ -220,7 +230,7 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
override fun ready() {
if (isLocally().not()) {
swingCoroutineScope.launch(Dispatchers.IO) { refreshToken() }
swingCoroutineScope.launch(Dispatchers.IO) { refresh() }
}
}
@@ -228,8 +238,34 @@ class AccountManager private constructor() : ApplicationRunnerExtension {
/**
* 刷新用户
*/
fun refresh(accessToken: String = getAccessToken()) {
fun refresh() {
runCatching { refreshToken() }.onSuccess {
refreshAccount()
}.onFailure {
if (log.isErrorEnabled) {
log.error(it.message, it)
}
}
}
fun refreshAccount() {
try {
val me = serverManager.callMe(account.server, getAccessToken())
val teams = me.teams.map {
Team(
id = it.id,
name = it.name,
secretKey = RSA.decrypt(getPrivateKey(), Base64.decodeBase64(it.secretKey)),
role = it.role
)
}
// 重新登录
login(account.copy(teams = teams, subscriptions = me.subscriptions))
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}
class AccountApplicationRunnerExtension private constructor() : ApplicationRunnerExtension {

View File

@@ -75,8 +75,10 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
val subscription = accountManager.getSubscription()
val isFreePlan = accountManager.isFreePlan()
val isLocally = accountManager.isLocally()
val validTo = if (isFreePlan) "-" else if (subscription.endAt >= Long.MAX_VALUE)
I18n.getString("termora.settings.account.lifetime") else
val validTo = if (isFreePlan) "-"
else if (subscription.endAt >= Long.MAX_VALUE)
I18n.getString("termora.settings.account.lifetime")
else
DateFormatUtils.format(Date(subscription.endAt), I18n.getString("termora.date-format"))
val lastSynchronizationOn = if (isFreePlan) "-" else
DateFormatUtils.format(
@@ -158,9 +160,19 @@ class AccountOption : JPanel(BorderLayout()), OptionsPane.Option, Disposable {
if (isFreePlan.not()) {
actions.add(JXHyperlink(object : AnAction(I18n.getString("termora.settings.account.sync-now")) {
override fun actionPerformed(evt: AnActionEvent) {
// 全量同步
accountProperties.nextSynchronizationSince = 0
// 拉取
PullService.getInstance().trigger()
// 推送
PushService.getInstance().trigger()
swingCoroutineScope.launch(Dispatchers.IO) {
// 刷新账户
accountManager.refreshAccount()
withContext(Dispatchers.Swing) {
isEnabled = false
lastSynchronizationOnLabel.text = DateFormatUtils.format(

View File

@@ -2,5 +2,4 @@ package app.termora.account
import app.termora.database.OwnerType
data class AccountOwner(val id: String, val name: String, val type: OwnerType) {
}
data class AccountOwner(val id: String, val name: String, val type: OwnerType)

View File

@@ -48,6 +48,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
Server(I18n.getString("termora.settings.account.server-singapore"), "https://account.termora.app")
private val chinaServer =
Server(I18n.getString("termora.settings.account.server-china"), "https://account.termora.cn")
private val serverManager get() = ServerManager.getInstance()
init {
isModal = true
@@ -359,7 +360,7 @@ class LoginServerDialog(owner: Window) : DialogWrapper(owner) {
val loginJob = swingCoroutineScope.launch(Dispatchers.IO) {
try {
ServerManager.getInstance().login(
serverManager.login(
server, usernameTextField.text,
String(passwordField.password), mfaTextField.text.trim()
)

View File

@@ -45,6 +45,15 @@ class PullService private constructor() : SyncService(), Disposable, Application
lastChangeHash = StringUtils.EMPTY
}
// 团队变了,全量同步
if (oldAccount.id == newAccount.id) {
if (oldAccount.teams != newAccount.teams) {
accountProperties.nextSynchronizationSince = 0
trigger()
return
}
}
if (oldAccount.isLocally && newAccount.isLocally.not()) {
trigger()
}
@@ -213,7 +222,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
log.debug("拉取数据: {} 成功, 响应码: {}", id, response.code)
}
if(response.isSuccessful.not()){
if (response.isSuccessful.not()) {
IOUtils.closeQuietly(response)
}
@@ -281,7 +290,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
ownerId = ownerId,
ownerType = ownerType,
type = type,
data = decryptData(id, data),
data = decryptData(id, data, ownerId),
version = version,
// 因为已经是拉取最新版本了,所以这里无需再同步了
synced = true,
@@ -298,11 +307,8 @@ class PullService private constructor() : SyncService(), Disposable, Application
if (log.isDebugEnabled) {
log.debug("数据: {}, 类型: {} 云端已经删除,本地即将删除", id, type)
}
databaseManager.delete(
id, type,
DatabaseChangedExtension.Source.Sync
)
databaseManager.delete(id, type, DatabaseChangedExtension.Source.Sync)
if (log.isInfoEnabled) {
log.info("数据: {}, 类型: {} 已从本地删除", id, type)
@@ -340,7 +346,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
ownerId = ownerId,
ownerType = ownerType,
type = type,
data = decryptData(id, data),
data = decryptData(id, data, ownerId),
version = version,
// 因为已经是拉取最新版本了,所以这里无需再同步了
synced = true,
@@ -377,7 +383,7 @@ class PullService private constructor() : SyncService(), Disposable, Application
pullChanges()
// N 秒拉一次
val result = withTimeoutOrNull(Random.nextInt(5, 15).seconds) {
val result = withTimeoutOrNull(Random.nextInt(3, 10).seconds) {
channel.receiveCatching()
} ?: continue
if (result.isFailure) break

View File

@@ -4,6 +4,7 @@ import app.termora.*
import app.termora.Application.ohMyJson
import app.termora.database.Data
import app.termora.database.DatabaseChangedExtension
import app.termora.database.OwnerType
import app.termora.plugin.DispatchThread
import app.termora.plugin.internal.extension.DynamicExtensionHandler
import kotlinx.coroutines.Dispatchers
@@ -106,7 +107,20 @@ class PushService private constructor() : SyncService(), Disposable, Application
.delete()
.build()
try {
AccountHttp.execute(request = request)
} catch (e: Exception) {
if (e is ResponseException) {
if (e.code == 403) {
// 如果是 Team 发现没有权限,那么很有可能是被提出团队
if (data.ownerType == OwnerType.Team.name) {
// 刷新用户
accountManager.refreshAccount()
}
}
}
throw e
}
// 修改为已经同步
updateData(data.id, synced = true)
@@ -153,6 +167,12 @@ class PushService private constructor() : SyncService(), Disposable, Application
}
// 标记为已经同步
updateData(data.id, synced = true, version = data.version)
// 如果是 Team 发现没有权限,那么很有可能是被提出团队
if (data.ownerType == OwnerType.Team.name) {
// 刷新用户
accountManager.refreshAccount()
}
return
} else if (response.code == 409) { // 版本冲突,一般来说是云端版本大于本地版本
val json = ohMyJson.decodeFromString<JsonObject>(text)

View File

@@ -54,7 +54,7 @@ class ServerManager private constructor() {
val loginResponse = callLogin(serverInfo, server, username, password, mfa)
// call me
val meResponse = callMe(server, loginResponse.accessToken)
val meResponse = callMe(server.server, loginResponse.accessToken)
// 解密
val salt = "${serverInfo.salt}:${username}".toByteArray()
@@ -139,9 +139,9 @@ class ServerManager private constructor() {
}
private fun callMe(server: Server, accessToken: String): MeResponse {
fun callMe(server: String, accessToken: String): MeResponse {
val request = Request.Builder()
.url("${server.server}/v1/users/me")
.url("${server}/v1/users/me")
.header("Authorization", "Bearer $accessToken")
.build()
val text = AccountHttp.execute(request = request)
@@ -149,13 +149,13 @@ class ServerManager private constructor() {
}
@Serializable
private data class ServerInfo(val salt: String)
data class ServerInfo(val salt: String)
@Serializable
private data class LoginResponse(val accessToken: String, val refreshToken: String)
data class LoginResponse(val accessToken: String, val refreshToken: String)
@Serializable
private data class MeResponse(
data class MeResponse(
val id: String,
val email: String,
val publicKey: String,
@@ -167,5 +167,5 @@ class ServerManager private constructor() {
@Serializable
private data class MeTeam(val id: String, val name: String, val role: TeamRole, val secretKey: String)
data class MeTeam(val id: String, val name: String, val role: TeamRole, val secretKey: String)
}

View File

@@ -78,28 +78,23 @@ abstract class SyncService {
protected fun encryptData(id: String, data: String, ownerId: String): String {
val iv = DigestUtils.sha256(id).copyOf(12)
var secretKey = EMPTY_BYTE_ARRAY
if (ownerId != accountManager.getAccountId()) {
val team = accountManager.getTeams().firstOrNull { it.id == ownerId }
if (team == null) {
return StringUtils.EMPTY
} else {
secretKey = team.secretKey
}
} else if (ownerId == accountManager.getAccountId()) {
secretKey = accountManager.getSecretKey()
}
val secretKey = getSecretKey(ownerId)
if (secretKey.isEmpty()) return StringUtils.EMPTY
return Base64.encodeBase64String(AES.GCM.encrypt(secretKey, iv, data.toByteArray()))
}
protected fun decryptData(id: String, data: String): String {
protected fun getSecretKey(ownerId: String): ByteArray {
if (ownerId == accountManager.getAccountId()) {
return accountManager.getSecretKey()
}
val team = accountManager.getTeams().firstOrNull { it.id == ownerId }
return team?.secretKey ?: EMPTY_BYTE_ARRAY
}
protected fun decryptData(id: String, data: String, ownerId: String): String {
val iv = DigestUtils.sha256(id).copyOf(12)
return String(
AES.GCM.decrypt(
accountManager.getSecretKey(), iv,
Base64.decodeBase64(data)
)
)
val secretKey = getSecretKey(ownerId)
if (secretKey.isEmpty()) throw IllegalStateException("根据 ownerId 无法获取对应密钥")
return String(AES.GCM.decrypt(secretKey, iv, Base64.decodeBase64(data)))
}
}

View File

@@ -26,4 +26,22 @@ class Team(
* 所属角色
*/
val role: TeamRole,
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Team
if (id != other.id) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + name.hashCode()
return result
}
}

View File

@@ -1,6 +1,7 @@
package app.termora.actions
import app.termora.NewHostDialogV2
import app.termora.account.AccountManager
import app.termora.tree.HostTreeNode
import javax.swing.tree.TreePath
@@ -14,6 +15,8 @@ class NewHostAction : AnAction() {
}
private val accountManager get() = AccountManager.getInstance()
override fun actionPerformed(evt: AnActionEvent) {
val tree = evt.getData(DataProviders.Welcome.HostTree) ?: return
var lastNode = (tree.lastSelectedPathComponent ?: tree.model.root) as? HostTreeNode ?: return
@@ -27,7 +30,7 @@ class NewHostAction : AnAction() {
}
val lastHost = lastNode.host
val dialog = NewHostDialogV2(evt.window)
val dialog = NewHostDialogV2(evt.window, accountOwner = accountManager.getOwners().first { it.id == lastHost.ownerId })
dialog.setLocationRelativeTo(evt.window)
dialog.isVisible = true
val host = (dialog.host ?: return).copy(

View File

@@ -11,7 +11,6 @@ import app.termora.highlight.KeywordHighlightManager
import app.termora.keymap.KeymapManager
import app.termora.keymgr.KeyManager
import app.termora.macro.MacroManager
import app.termora.plugin.ExtensionManager
import app.termora.plugin.internal.extension.DynamicExtensionHandler
import app.termora.snippet.SnippetManager
import app.termora.terminal.CursorStyle
@@ -23,6 +22,7 @@ import org.jetbrains.exposed.v1.core.statements.StatementType
import org.jetbrains.exposed.v1.jdbc.*
import org.jetbrains.exposed.v1.jdbc.transactions.TransactionManager
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.*
import java.util.concurrent.locks.ReentrantLock
@@ -32,11 +32,8 @@ import kotlin.reflect.KProperty
class DatabaseManager private constructor() : Disposable {
companion object {
val log: Logger = LoggerFactory.getLogger(DatabaseManager::class.java)
private const val DB_PASSWORD = "DB_PASSWORD"
private const val DB_SALT = "DB_SALT"
val log = LoggerFactory.getLogger(DatabaseManager::class.java)!!
fun getInstance(): DatabaseManager {
return ApplicationScope.forApplicationScope()
.getOrCreate(DatabaseManager::class) { DatabaseManager() }
@@ -52,14 +49,6 @@ class DatabaseManager private constructor() : Disposable {
val sftp by lazy { SFTP(this) }
@Volatile
internal var dbPassword = StringUtils.EMPTY
private set
@Volatile
internal var dbSalt = StringUtils.EMPTY
private set
private val map = Collections.synchronizedMap<String, String?>(mutableMapOf())
private val accountManager get() = AccountManager.getInstance()
@@ -101,11 +90,6 @@ class DatabaseManager private constructor() : Disposable {
// 注册动态扩展
registerDynamicExtensions()
for (extension in ExtensionManager.getInstance().getExtensions(DatabaseReadyExtension::class.java)) {
extension.ready(this)
}
}
private fun registerDynamicExtensions() {
@@ -308,6 +292,7 @@ class DatabaseManager private constructor() : Disposable {
DataEntity.update({ DataEntity.id eq id }) {
it[DataEntity.deleted] = true
// 如果是本地用户,那么删除是不需要同步的,云端用户才需要同步
// 云端用户也会判断,如果来源 Sync 那么默认同步了
it[DataEntity.synced] = accountManager.isLocally()
it[DataEntity.data] = StringUtils.EMPTY
}
@@ -367,7 +352,6 @@ class DatabaseManager private constructor() : Disposable {
private inner class AccountDataTransferExtension : AccountExtension {
private val hostManager get() = HostManager.getInstance()
override fun onAccountChanged(oldAccount: Account, newAccount: Account) {
if (oldAccount.isLocally && newAccount.isLocally) {
return
@@ -481,13 +465,10 @@ class DatabaseManager private constructor() : Disposable {
return
}
// 如果团队变更,那么删除所有旧的团队数据,静默删除
if (oldAccount.id == newAccount.id) {
return
}
if (oldAccount.teams != newAccount.teams) {
for (team in oldAccount.teams) {
// 如果被踢出团队,那么移除该团队的所有资产
if (newAccount.teams.none { it.id == team.id }) {
lock.withLock {
transaction(database) {
DataEntity.deleteWhere {
@@ -499,6 +480,7 @@ class DatabaseManager private constructor() : Disposable {
}
}
}
}
}

View File

@@ -1,37 +0,0 @@
package app.termora.database
import app.termora.database.DatabaseManager.Companion.log
import app.termora.plugin.DispatchThread
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionManager
import javax.swing.SwingUtilities
interface DatabaseReadyExtension : Extension {
companion object {
fun fireReady(databaseManager: DatabaseManager) {
if (SwingUtilities.isEventDispatchThread()) {
for (extension in ExtensionManager.getInstance().getExtensions(DatabaseReadyExtension::class.java)) {
try {
extension.ready(databaseManager)
} catch (e: Exception) {
if (log.isErrorEnabled) {
log.error(e.message, e)
}
}
}
} else {
SwingUtilities.invokeLater { fireReady(databaseManager) }
}
}
}
/**
* 数据库初始化完成
*/
fun ready(databaseManager: DatabaseManager) {}
override fun getDispatchThread(): DispatchThread {
return DispatchThread.BGT
}
}

View File

@@ -19,6 +19,7 @@ class KeyManagerDialog(
owner: Window,
private val selectMode: Boolean = false,
size: Dimension = Dimension(UIManager.getInt("Dialog.width"), UIManager.getInt("Dialog.height")),
private val accountOwner: AccountOwner? = null,
) : DialogWrapper(owner) {
var ok: Boolean = false
@@ -56,12 +57,40 @@ class KeyManagerDialog(
tabbed.border = BorderFactory.createMatteBorder(1, 0, 0, 0, DynamicColor.BorderColor)
tabbed.tabPlacement = JTabbedPane.TOP
if (accountOwner == null || accountOwner.type == OwnerType.User) {
tabbed.addTab(
I18n.getString("termora.keymgr.my-keys"),
Icons.user,
KeyManagerPanel(AccountOwner(accountManager.getAccountId(), accountManager.getEmail(), OwnerType.User))
KeyManagerPanel(
AccountOwner(
accountManager.getAccountId(),
accountManager.getEmail(),
OwnerType.User
)
)
)
}
if (accountOwner != null && accountManager.hasTeamFeature()) {
for (team in accountManager.getTeams()) {
if (team.id == accountOwner.id) {
tabbed.addTab(
team.name,
Icons.cwmUsers,
KeyManagerPanel(
AccountOwner(
team.id,
team.name,
OwnerType.Team
)
)
)
return tabbed
}
}
}
if (accountManager.hasTeamFeature()) {
for (team in accountManager.getTeams()) {

View File

@@ -1,5 +1,6 @@
package app.termora.plugin.internal.local
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -14,7 +15,7 @@ internal class LocalProtocolHostPanelExtension private constructor() : ProtocolH
return LocalProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return LocalProtocolHostPanel()
}
}

View File

@@ -1,5 +1,6 @@
package app.termora.plugin.internal.rdp
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -14,7 +15,7 @@ internal class RDPProtocolHostPanelExtension private constructor() : ProtocolHos
return RDPProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return RDPProtocolHostPanel()
}
}

View File

@@ -1,5 +1,6 @@
package app.termora.plugin.internal.serial
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -14,7 +15,7 @@ internal class SerialProtocolHostPanelExtension private constructor() : Protocol
return SerialProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return SerialProtocolHostPanel()
}

View File

@@ -1,6 +1,7 @@
package app.termora.plugin.internal.ssh
import app.termora.*
import app.termora.account.AccountOwner
import app.termora.keymgr.KeyManager
import app.termora.keymgr.KeyManagerDialog
import app.termora.plugin.internal.BasicProxyOption
@@ -29,7 +30,7 @@ import javax.swing.table.DefaultTableModel
import kotlin.math.max
@Suppress("CascadeIf")
open class SSHHostOptionsPane : OptionsPane() {
open class SSHHostOptionsPane(private val accountOwner: AccountOwner) : OptionsPane() {
protected val tunnelingOption = TunnelingOption()
protected val generalOption = GeneralOption()
protected val proxyOption = BasicProxyOption()
@@ -375,6 +376,7 @@ open class SSHHostOptionsPane : OptionsPane() {
val dialog = KeyManagerDialog(
owner,
selectMode = true,
accountOwner = accountOwner,
)
dialog.pack()
dialog.setLocationRelativeTo(owner)
@@ -383,7 +385,7 @@ open class SSHHostOptionsPane : OptionsPane() {
val selectedItem = publicKeyComboBox.selectedItem
publicKeyComboBox.removeAllItems()
for (keyPair in KeyManager.getInstance().getOhKeyPairs()) {
for (keyPair in KeyManager.getInstance().getOhKeyPairs(accountOwner.id)) {
publicKeyComboBox.addItem(keyPair.id)
}
publicKeyComboBox.selectedItem = selectedItem
@@ -465,7 +467,7 @@ open class SSHHostOptionsPane : OptionsPane() {
if (authenticationTypeComboBox.selectedItem == AuthenticationType.PublicKey) {
val selectedItem = publicKeyComboBox.selectedItem
publicKeyComboBox.removeAllItems()
for (pair in KeyManager.getInstance().getOhKeyPairs()) {
for (pair in KeyManager.getInstance().getOhKeyPairs(accountOwner.id)) {
publicKeyComboBox.addItem(pair.id)
}
publicKeyComboBox.selectedItem = selectedItem

View File

@@ -2,12 +2,13 @@ package app.termora.plugin.internal.ssh
import app.termora.Disposer
import app.termora.Host
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import java.awt.BorderLayout
class SSHProtocolHostPanel : ProtocolHostPanel() {
class SSHProtocolHostPanel(accountOwner: AccountOwner) : ProtocolHostPanel() {
private val pane = SSHHostOptionsPane()
private val pane = SSHHostOptionsPane(accountOwner)
init {
initView()

View File

@@ -1,5 +1,6 @@
package app.termora.plugin.internal.ssh
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -14,8 +15,8 @@ internal class SSHProtocolHostPanelExtension private constructor() : ProtocolHos
return SSHProtocolProvider.instance
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
return SSHProtocolHostPanel()
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return SSHProtocolHostPanel(accountOwner)
}
}

View File

@@ -1,5 +1,6 @@
package app.termora.plugin.internal.wsl
import app.termora.account.AccountOwner
import app.termora.protocol.ProtocolHostPanel
import app.termora.protocol.ProtocolHostPanelExtension
import app.termora.protocol.ProtocolProvider
@@ -14,11 +15,11 @@ internal class WSLProtocolHostPanelExtension private constructor() : ProtocolHos
return WSLProtocolProvider.instance
}
override fun canCreateProtocolHostPanel(): Boolean {
override fun canCreateProtocolHostPanel(accountOwner: AccountOwner): Boolean {
return WSLSupport.isSupported
}
override fun createProtocolHostPanel(): ProtocolHostPanel {
override fun createProtocolHostPanel(accountOwner: AccountOwner): ProtocolHostPanel {
return WSLProtocolHostPanel()
}
}

View File

@@ -1,8 +1,10 @@
package app.termora.protocol
import app.termora.account.AccountOwner
import app.termora.plugin.Extension
import app.termora.plugin.ExtensionManager
interface ProtocolHostPanelExtension : Extension {
companion object {
val extensions
@@ -19,11 +21,23 @@ interface ProtocolHostPanelExtension : Extension {
/**
* 是否可以创建协议主机面板
*/
@Deprecated("Old stuff")
fun canCreateProtocolHostPanel(): Boolean = true
/**
* 是否可以创建协议主机面板
*/
fun canCreateProtocolHostPanel(accountOwner: AccountOwner) = canCreateProtocolHostPanel()
/**
* 创建协议主机面板
*/
fun createProtocolHostPanel(): ProtocolHostPanel
@Deprecated("Old stuff")
fun createProtocolHostPanel(): ProtocolHostPanel = throw UnsupportedOperationException()
/**
* 创建协议主机面板
*/
fun createProtocolHostPanel(accountOwner: AccountOwner) = createProtocolHostPanel()
}

View File

@@ -1,6 +1,7 @@
package app.termora.transfer
import app.termora.*
import app.termora.account.AccountManager
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.database.DatabaseChangedExtension
@@ -164,9 +165,13 @@ class TransportTabbed(
// 编辑
val edit = popupMenu.add(I18n.getString("termora.keymgr.edit"))
edit.addActionListener(object : AnAction() {
private val accountManager get() = AccountManager.getInstance()
override fun actionPerformed(evt: AnActionEvent) {
val window = evt.window
val dialog = NewHostDialogV2(window, panel.host)
val dialog = NewHostDialogV2(
window,
panel.host,
accountOwner = accountManager.getOwners().first { it.id == panel.host.ownerId })
dialog.setLocationRelativeTo(window)
dialog.title = panel.host.name
dialog.isVisible = true

View File

@@ -2,6 +2,7 @@ package app.termora.tree
import app.termora.*
import app.termora.Application.ohMyJson
import app.termora.account.AccountManager
import app.termora.actions.OpenHostAction
import app.termora.database.DatabaseChangedExtension
import app.termora.database.DatabaseManager
@@ -295,8 +296,11 @@ class NewHostTree : SimpleTree(), Disposable {
}
}
newHost.addActionListener(object : ActionListener {
private val accountManager get() = AccountManager.getInstance()
override fun actionPerformed(e: ActionEvent) {
val dialog = NewHostDialogV2(owner)
val dialog = NewHostDialogV2(
owner,
accountOwner = accountManager.getOwners().first { it.id == lastHost.ownerId })
dialog.setLocationRelativeTo(owner)
dialog.isVisible = true
val host = (dialog.host ?: return).copy(
@@ -311,8 +315,12 @@ class NewHostTree : SimpleTree(), Disposable {
}
})
property.addActionListener(object : ActionListener {
private val accountManager get() = AccountManager.getInstance()
override fun actionPerformed(e: ActionEvent) {
val dialog = NewHostDialogV2(owner, lastHost)
val dialog = NewHostDialogV2(
owner,
lastHost,
accountOwner = accountManager.getOwners().first { it.id == lastHost.ownerId })
dialog.setLocationRelativeTo(owner)
dialog.title = lastHost.name
dialog.isVisible = true
@@ -639,12 +647,12 @@ class NewHostTree : SimpleTree(), Disposable {
ownerType = folder.host.ownerType,
ownerId = folder.host.ownerId,
),
DatabaseChangedExtension.Source.Sync
DatabaseChangedExtension.Source.User
)
for (host in node.getAllChildren().map { it.host }) {
hostManager.addHost(
host.copy(ownerType = folder.host.ownerType, ownerId = folder.host.ownerId),
DatabaseChangedExtension.Source.Sync
DatabaseChangedExtension.Source.User
)
}
}

View File

@@ -148,6 +148,10 @@ class NewHostTreeModel private constructor() : SimpleTreeModel<Host>(
}
hostManager.removeHost(node.id)
}
removeNodeFromParent0(node)
}
private fun removeNodeFromParent0(node: MutableTreeNode?) {
super.removeNodeFromParent(node)
}
@@ -232,7 +236,13 @@ class NewHostTreeModel private constructor() : SimpleTreeModel<Host>(
private inner class MyAccountAccountExtension : AccountExtension {
override fun onAccountChanged(oldAccount: Account, newAccount: Account) {
if (oldAccount.id != newAccount.id) reload(getRoot())
if (oldAccount.id != newAccount.id) {
reload(getRoot())
} else if (oldAccount.teams != newAccount.teams) {
val nodes = getRoot().children().toList().filterIsInstance<TeamTreeNode>()
nodes.forEach { removeNodeFromParent0(it) }
reload(getRoot())
}
}
}