feat: support app and image synchronization (#13067)

This commit is contained in:
ssongliu
2026-06-17 15:41:09 +08:00
committed by GitHub
parent 7f331f5503
commit 46af88e1d0
44 changed files with 684 additions and 100 deletions
+6 -1
View File
@@ -413,10 +413,15 @@ func (b *BaseApi) Backup(c *gin.Context) {
switch req.Type {
case "app":
if _, err := backupService.AppBackup(req); err != nil {
record, err := backupService.AppBackup(req)
if err != nil {
helper.InternalServer(c, err)
return
}
if req.IsImmediate {
helper.SuccessWithData(c, record)
return
}
case "mysql", "mariadb", constant.AppMysqlCluster:
if err := backupService.MysqlBackup(req); err != nil {
helper.InternalServer(c, err)
+9 -8
View File
@@ -65,14 +65,15 @@ type UploadForRecover struct {
}
type CommonBackup struct {
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"`
Name string `json:"name"`
DetailName string `json:"detailName"`
Secret string `json:"secret"`
StopBefore bool `json:"stopBefore"`
TaskID string `json:"taskID"`
FileName string `json:"fileName"`
Args []string `json:"args"`
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"`
Name string `json:"name"`
DetailName string `json:"detailName"`
Secret string `json:"secret"`
IsImmediate bool `json:"isImmediate"`
StopBefore bool `json:"stopBefore"`
TaskID string `json:"taskID"`
FileName string `json:"fileName"`
Args []string `json:"args"`
Description string `json:"description"`
}
+1
View File
@@ -131,6 +131,7 @@ type AppInstallDTO struct {
Container string `json:"container"`
IsEdit bool `json:"isEdit"`
LinkDB bool `json:"linkDB"`
ResourceKeys []string `json:"resourceKeys"`
ServiceName string `json:"serviceName"`
}
+27 -13
View File
@@ -1620,6 +1620,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
synAppInstall(containersMap, &installed, false)
}
resourceKeys := getAppInstallResourceKeys(installed.ID)
installDTO := response.AppInstallDTO{
ID: installed.ID,
Name: installed.Name,
@@ -1641,14 +1642,15 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
Website: installed.App.Website,
Document: installed.App.Document,
},
Favorite: installed.Favorite,
SortOrder: installed.SortOrder,
Container: installed.ContainerName,
ServiceName: strings.ToLower(installed.ServiceName),
Favorite: installed.Favorite,
SortOrder: installed.SortOrder,
Container: installed.ContainerName,
ServiceName: strings.ToLower(installed.ServiceName),
ResourceKeys: resourceKeys,
}
if !updated && !checkUpdate {
installDTO.LinkDB = hasLinkDB(installed.ID)
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
res = append(res, installDTO)
continue
}
@@ -1656,7 +1658,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
if installed.Version == "latest" {
if checkUpdate {
installDTO.CanUpdate = false
installDTO.LinkDB = hasLinkDB(installed.ID)
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
res = append(res, installDTO)
}
continue
@@ -1685,7 +1687,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
if len(versions) == 0 {
if checkUpdate {
installDTO.CanUpdate = false
installDTO.LinkDB = hasLinkDB(installed.ID)
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
res = append(res, installDTO)
}
continue
@@ -1717,7 +1719,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
res = append(res, installDTO)
}
} else if checkUpdate {
installDTO.LinkDB = hasLinkDB(installed.ID)
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
res = append(res, installDTO)
}
}
@@ -2284,13 +2286,25 @@ func needsUpdate(localTag *model.Tag, remoteTag dto.Tag, translations string) bo
}
func hasLinkDB(installID uint) bool {
return hasLinkDBFromKeys(getAppInstallResourceKeys(installID))
}
func getAppInstallResourceKeys(installID uint) []string {
resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(installID))
keys := make([]string, 0, len(resources))
for _, resource := range resources {
keys = append(keys, resource.Key)
}
return keys
}
func hasLinkDBFromKeys(resourceKeys []string) bool {
hasDB := false
if len(resources) > 0 {
for _, resource := range resources {
if resource.Key == constant.AppPostgres || resource.Key == constant.AppMysql ||
resource.Key == constant.AppMariaDB || resource.Key == constant.AppMysqlCluster ||
resource.Key == constant.AppPostgresql || resource.Key == constant.AppPostgresqlCluster {
if len(resourceKeys) > 0 {
for _, resourceKey := range resourceKeys {
if resourceKey == constant.AppPostgres || resourceKey == constant.AppMysql ||
resourceKey == constant.AppMariaDB || resourceKey == constant.AppMysqlCluster ||
resourceKey == constant.AppPostgresql || resourceKey == constant.AppPostgresqlCluster {
hasDB = true
break
}
+22 -3
View File
@@ -63,11 +63,30 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, er
return nil, err
}
if err = handleAppBackup(&install, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil {
global.LOG.Errorf("backup app %s failed, err: %v", req.DetailName, err)
return nil, err
if !req.IsImmediate {
if err = handleAppBackup(&install, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil {
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
global.LOG.Errorf("backup app %s failed, err: %v", req.DetailName, err)
return nil, err
}
return record, nil
}
backupTask, err := task.NewTaskWithOps(install.Name, task.TaskBackup, task.TaskScopeBackup, req.TaskID, install.ID)
if err != nil {
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
record.Status = constant.StatusFailed
record.Message = err.Error()
return nil, err
}
if err = doAppBackup(&install, backupTask, backupDir, fileName, "", req.Secret); err != nil {
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
record.Status = constant.StatusFailed
record.Message = err.Error()
return nil, err
}
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusSuccess})
record.Status = constant.StatusSuccess
return record, nil
}
+1 -10
View File
@@ -2372,15 +2372,6 @@
"formatZH": "检查应用安装 [name]",
"formatEN": "check app install [name]"
},
"/custom/app/push": {
"bodyKeys": [
"appBackupID"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "推送应用备份 [appBackupID] 到节点",
"formatEN": "push app backup [appBackupID] to node"
},
"/custom/app/sync": {
"bodyKeys": [],
"paramKeys": [],
@@ -5092,4 +5083,4 @@
"formatZH": "从主节点同步设置",
"formatEN": "sync settings from master"
}
}
}
+1
View File
@@ -108,6 +108,7 @@ ErrContainerMsg: 'Container {{ .name }} is abnormal; check logs'
ErrAppBackup: 'App backup failed: {{ .name }} {{ .err }}'
ErrVersionTooLow: '1Panel version too low to update App Store'
ErrAppNameExist: 'The application name already exists'
ErrAppInstallNameExist: 'App install name {{ .name }} already exists'
AppStoreIsSyncing: 'App Store is syncing; try again later'
ErrGetCompose: 'Failed to read docker-compose.yml: {{ .detail }}'
ErrAppWarn: 'App status abnormal; check logs'
+1
View File
@@ -107,6 +107,7 @@ ErrContainerMsg: 'El contenedor {{ .name }} presenta anomalías. Revise el log e
ErrAppBackup: 'Falló el respaldo de la aplicación {{ .name }} {{ .err }}'
ErrVersionTooLow: 'La versión actual de 1Panel es demasiado baja para actualizar la tienda de aplicaciones. Actualice primero.'
ErrAppNameExist: 'El nombre de la aplicación ya existe'
ErrAppInstallNameExist: 'El nombre de instalación de la aplicación {{ .name }} ya existe'
AppStoreIsSyncing: 'La tienda de aplicaciones está sincronizando, inténtelo más tarde'
ErrGetCompose: 'Fallo al obtener el archivo docker-compose.yml {{ .detail }}'
ErrAppWarn: 'Estado anómalo, revise el log'
+1
View File
@@ -107,6 +107,7 @@ ErrContainerMsg: '{{ .name }} コンテナが異常です。詳細について
ErrAppBackup: '{{ .name }} アプリケーションのバックアップに失敗しました。エラー {{ .err }}'
ErrVersionTooLow: '現在の 1Panel のバージョンが低すぎるため、App Store を更新できません。操作する前にバージョンをアップグレードしてください。'
ErrAppNameExist: 'アプリケーション名がすでに存在します'
ErrAppInstallNameExist: 'アプリケーションインストール名 {{ .name }} はすでに存在します'
AppStoreIsSyncing: 'App Store が同期中です。しばらくしてからもう一度お試しください'
ErrGetCompose: 'docker-compose.yml ファイルの取得に失敗しました {{ .detail }}'
ErrAppWarn: '異常な状態です。ログを確認してください'
+1
View File
@@ -107,6 +107,7 @@ ErrContainerMsg: '{{ .name }} 컨테이너가 비정상입니다. 자세한 내
ErrAppBackup: '{{ .name }} 애플리케이션 백업에 실패했습니다 {{ .err }}'
ErrVersionTooLow: '현재 1Panel 버전이 너무 낮아 App Store를 업데이트할 수 없습니다. 작동하시기 전에 버전을 업그레이드하세요.'
ErrAppNameExist: '응용 프로그램 이름이 이미 존재합니다'
ErrAppInstallNameExist: '앱 설치 이름 {{ .name }} 이(가) 이미 존재합니다'
AppStoreIsSyncing: 'App Store가 동기화 중입니다. 나중에 다시 시도하세요.'
ErrGetCompose: 'docker-compose.yml 파일을 가져오지 못했습니다 {{ .detail }}'
ErrAppWarn: '비정상적인 상태입니다. 로그를 확인해 주세요.'
+1
View File
@@ -107,6 +107,7 @@ ErrContainerMsg: 'Bekas ''{{ .name }} adalah tidak normal. Sila semak log pada h
ErrAppBackup: '{{ .name }} sandaran aplikasi gagal {{ .err }}'
ErrVersionTooLow: 'Versi 1Panel semasa terlalu rendah untuk mengemas kini App Store. Sila tingkatkan versi sebelum beroperasi.'
ErrAppNameExist: 'Nama aplikasi sudah wujud'
ErrAppInstallNameExist: 'Nama pemasangan aplikasi {{ .name }} sudah wujud'
AppStoreIsSyncing: 'App Store sedang menyegerak, sila cuba sebentar lagi'
ErrGetCompose: 'Gagal mendapatkan fail docker-compose.yml {{ .detail }}'
ErrAppWarn: 'Status tidak normal, sila semak log'
+1
View File
@@ -107,6 +107,7 @@ ErrContainerMsg: 'O contêiner {{ .name }} está anormal., verifique o log na p
ErrAppBackup: '{{ .name }} falha no backup do aplicativo {{ .err }}'
ErrVersionTooLow: 'A versão atual do 1Panel é muito antiga para atualizar a App Store., atualize a versão antes de operar.'
ErrAppNameExist: 'O nome do aplicativo já existe'
ErrAppInstallNameExist: 'O nome de instalação do aplicativo {{ .name }} já existe'
AppStoreIsSyncing: 'A App Store está sincronizando mais tarde'
ErrGetCompose: 'Falha ao obter o arquivo docker-compose.yml {{ .detail }}'
ErrAppWarn: 'Status anormal, verifique o log'
+1
View File
@@ -107,6 +107,7 @@ ErrContainerMsg: 'Контейнер {{ .name }} ненормален. Подр
ErrAppBackup: '{{ .name }} резервное копирование приложения не удалось {{ .err }}'
ErrVersionTooLow: 'Текущая версия 1Panel слишком низкая для обновления App Store. Пожалуйста, обновите версию перед началом работы.'
ErrAppNameExist: 'Имя приложения уже существует'
ErrAppInstallNameExist: 'Имя установки приложения {{ .name }} уже существует'
AppStoreIsSyncing: 'App Store синхронизируется, повторите попытку позже'
ErrGetCompose: 'Не удалось получить файл docker-compose.yml {{ .detail }}'
ErrAppWarn: 'Ненормальное состояние, проверьте журнал'
+1
View File
@@ -107,6 +107,7 @@ ErrContainerMsg: '{{ .name }} konteyneri anormal. Lütfen ayrıntılar için kon
ErrAppBackup: '{{ .name }} uygulama yedeklemesi başarısız {{ .err }}'
ErrVersionTooLow: 'Mevcut 1Panel sürümü Uygulama Mağazasını güncellemek için çok düşük. Lütfen işlemden önce sürümü yükseltin.'
ErrAppNameExist: 'Uygulama adı zaten mevcut'
ErrAppInstallNameExist: '{{ .name }} uygulama kurulum adı zaten mevcut'
AppStoreIsSyncing: 'Uygulama Mağazası senkronize ediliyor, lütfen daha sonra tekrar deneyin'
ErrGetCompose: 'docker-compose.yml dosyası alınamadı {{ .detail }}'
ErrAppWarn: 'Anormal durum, lütfen günlüğü kontrol edin'
+1
View File
@@ -107,6 +107,7 @@ ErrContainerMsg: '{{ .name }} 容器異常,請在容器頁面上檢視日誌'
ErrAppBackup: '{{ .name }} 應用備份失敗 {{ .err }}'
ErrVersionTooLow: '目前 1Panel 版本過低,無法更新應用程式商店,請升級版本之後操作'
ErrAppNameExist: '應用程式名稱已存在'
ErrAppInstallNameExist: '應用程式安裝名稱 {{ .name }} 已存在'
AppStoreIsSyncing: '應用程式商店正在同步中,請稍後重試'
ErrGetCompose: 'docker-compose.yml 檔案取得失敗!{{ .detail }}'
ErrAppWarn: '狀態異常,請檢視日誌'
+1
View File
@@ -108,6 +108,7 @@ ErrContainerMsg: '{{ .name }} 容器异常,具体请在容器页面查看日
ErrAppBackup: '{{ .name }} 应用备份失败 {{ .err }}'
ErrVersionTooLow: '当前 1Panel 版本过低,无法更新应用商店,请升级版本之后操作'
ErrAppNameExist: '应用名称已存在'
ErrAppInstallNameExist: '应用安装名称 {{ .name }} 已存在'
AppStoreIsSyncing: '应用商店同步中,请稍后重试'
ErrGetCompose: "docker-compose.yml 文件获取失败!{{ .detail }}"
ErrAppWarn: "状态异常,请查看日志"
+1 -10
View File
@@ -2372,15 +2372,6 @@
"formatZH": "检查应用安装 [name]",
"formatEN": "check app install [name]"
},
"/custom/app/push": {
"bodyKeys": [
"appBackupID"
],
"paramKeys": [],
"beforeFunctions": [],
"formatZH": "推送应用备份 [appBackupID] 到节点",
"formatEN": "push app backup [appBackupID] to node"
},
"/custom/app/sync": {
"bodyKeys": [],
"paramKeys": [],
@@ -5092,4 +5083,4 @@
"formatZH": "从主节点同步设置",
"formatEN": "sync settings from master"
}
}
}
+4 -1
View File
@@ -183,7 +183,10 @@ var WebUrlMap = map[string]struct{}{
"/xpack/node/dashboard": {},
"/xpack/node": {},
"/xpack/simple-node": {},
"/xpack/exchange/file": {},
"/xpack/sync": {},
"/xpack/sync/image": {},
"/xpack/sync/cert": {},
"/xpack/sync/app": {},
"/xpack/app": {},
"/xpack/app-upgrade": {},
+19
View File
@@ -314,6 +314,25 @@ DBConnErr: "Error: Failed to initialize database connection, {{ .err }}"
SystemVersion: "version: "
SystemMode: "mode: "
#exchange
LocalNodeIpFailed: "Failed to get master node IP. Edit the master node and add IP address and SSH authentication information."
HandlePrivateKey: "Handle node private key"
HandlePublicKey: "Handle node public key"
ExchangeFile: "Start syncing {{ .sourcePath }} from node {{ .source }} to node {{ .dest }}:{{ .destPath }}"
TaskRsync: "Sync"
NodeFile: "Node file"
ImageSyncTaskName: "Image sync {{ .source }} -> {{ .count }} nodes"
ImageSyncSave: "Save image {{ .image }}"
ImageSyncSaveTags: "Save image tags {{ .tags }}"
ImageSyncToNode: "Sync image {{ .image }} to node {{ .node }}"
ImageSyncLoad: "Load image {{ .image }} on node {{ .node }}"
ImageSyncClean: "Clean temporary files for image {{ .image }}"
ImageSyncNoTag: "Image {{ .image }} has no tag and will be saved by image ID"
ImageSyncFrontendTagsIgnored: "No tags were found for image {{ .image }} on the source node. Frontend tags were ignored."
ImageSyncHandlePrivateKey: "Handle image sync node private key"
ImageSyncHandlePublicKey: "Handle image sync node public key"
ImageSyncTransfer: "Start transferring image temporary file: {{ .source }}:{{ .sourcePath }} -> {{ .dest }}:{{ .destPath }}"
#mobile app
ErrVerifyToken: 'Token verification failed. Reset and scan again.'
ErrInvalidToken: 'Invalid token. Reset and scan again.'
+19
View File
@@ -308,6 +308,25 @@ DBConnErr: "Error: Fallo al inicializar la conexión a la base de datos, {{ .err
SystemVersion: "versión: "
SystemMode: "modo: "
#exchange
LocalNodeIpFailed: "Failed to get master node IP. Edit the master node and add IP address and SSH authentication information."
HandlePrivateKey: "Handle node private key"
HandlePublicKey: "Handle node public key"
ExchangeFile: "Start syncing {{ .sourcePath }} from node {{ .source }} to node {{ .dest }}:{{ .destPath }}"
TaskRsync: "Sync"
NodeFile: "Node file"
ImageSyncTaskName: "Image sync {{ .source }} -> {{ .count }} nodes"
ImageSyncSave: "Save image {{ .image }}"
ImageSyncSaveTags: "Save image tags {{ .tags }}"
ImageSyncToNode: "Sync image {{ .image }} to node {{ .node }}"
ImageSyncLoad: "Load image {{ .image }} on node {{ .node }}"
ImageSyncClean: "Clean temporary files for image {{ .image }}"
ImageSyncNoTag: "Image {{ .image }} has no tag and will be saved by image ID"
ImageSyncFrontendTagsIgnored: "No tags were found for image {{ .image }} on the source node. Frontend tags were ignored."
ImageSyncHandlePrivateKey: "Handle image sync node private key"
ImageSyncHandlePublicKey: "Handle image sync node public key"
ImageSyncTransfer: "Start transferring image temporary file: {{ .source }}:{{ .sourcePath }} -> {{ .dest }}:{{ .destPath }}"
#mobile app
ErrVerifyToken: 'Error al verificar el token, por favor restablezca y escanee de nuevo.'
ErrInvalidToken: 'Token inválido, por favor restablezca y escanee de nuevo.'
+19
View File
@@ -308,6 +308,25 @@ DBConnErr: "エラー: データベース接続の初期化に失敗しました
SystemVersion: "バージョン: "
SystemMode: "モード: "
#exchange
LocalNodeIpFailed: "Failed to get master node IP. Edit the master node and add IP address and SSH authentication information."
HandlePrivateKey: "Handle node private key"
HandlePublicKey: "Handle node public key"
ExchangeFile: "Start syncing {{ .sourcePath }} from node {{ .source }} to node {{ .dest }}:{{ .destPath }}"
TaskRsync: "Sync"
NodeFile: "Node file"
ImageSyncTaskName: "Image sync {{ .source }} -> {{ .count }} nodes"
ImageSyncSave: "Save image {{ .image }}"
ImageSyncSaveTags: "Save image tags {{ .tags }}"
ImageSyncToNode: "Sync image {{ .image }} to node {{ .node }}"
ImageSyncLoad: "Load image {{ .image }} on node {{ .node }}"
ImageSyncClean: "Clean temporary files for image {{ .image }}"
ImageSyncNoTag: "Image {{ .image }} has no tag and will be saved by image ID"
ImageSyncFrontendTagsIgnored: "No tags were found for image {{ .image }} on the source node. Frontend tags were ignored."
ImageSyncHandlePrivateKey: "Handle image sync node private key"
ImageSyncHandlePublicKey: "Handle image sync node public key"
ImageSyncTransfer: "Start transferring image temporary file: {{ .source }}:{{ .sourcePath }} -> {{ .dest }}:{{ .destPath }}"
#mobile app
ErrVerifyToken: 'トークンの検証エラーです。リセット後、再度QRコードをスキャンしてください。'
ErrInvalidToken: '無効なトークンです。リセット後、再度QRコードをスキャンしてください。'
+19
View File
@@ -307,6 +307,25 @@ DBConnErr: "오류: 데이터베이스 연결 초기화 실패 {{ .err }}"
SystemVersion: "버전: "
SystemMode: "모드: "
#exchange
LocalNodeIpFailed: "Failed to get master node IP. Edit the master node and add IP address and SSH authentication information."
HandlePrivateKey: "Handle node private key"
HandlePublicKey: "Handle node public key"
ExchangeFile: "Start syncing {{ .sourcePath }} from node {{ .source }} to node {{ .dest }}:{{ .destPath }}"
TaskRsync: "Sync"
NodeFile: "Node file"
ImageSyncTaskName: "Image sync {{ .source }} -> {{ .count }} nodes"
ImageSyncSave: "Save image {{ .image }}"
ImageSyncSaveTags: "Save image tags {{ .tags }}"
ImageSyncToNode: "Sync image {{ .image }} to node {{ .node }}"
ImageSyncLoad: "Load image {{ .image }} on node {{ .node }}"
ImageSyncClean: "Clean temporary files for image {{ .image }}"
ImageSyncNoTag: "Image {{ .image }} has no tag and will be saved by image ID"
ImageSyncFrontendTagsIgnored: "No tags were found for image {{ .image }} on the source node. Frontend tags were ignored."
ImageSyncHandlePrivateKey: "Handle image sync node private key"
ImageSyncHandlePublicKey: "Handle image sync node public key"
ImageSyncTransfer: "Start transferring image temporary file: {{ .source }}:{{ .sourcePath }} -> {{ .dest }}:{{ .destPath }}"
#mobile app
ErrVerifyToken: '토큰 검증 오류가 발생했습니다. 재설정 후 다시 스캔해 주세요.'
ErrInvalidToken: '유효하지 않은 토큰입니다. 재설정 후 다시 스캔해 주세요.'
+19
View File
@@ -302,6 +302,25 @@ DBConnErr: "Ralat: Gagal memulakan sambungan pangkalan data, {{ .err }}"
SystemVersion: "Versi: "
SystemMode: "Mod: "
#exchange
LocalNodeIpFailed: "Failed to get master node IP. Edit the master node and add IP address and SSH authentication information."
HandlePrivateKey: "Handle node private key"
HandlePublicKey: "Handle node public key"
ExchangeFile: "Start syncing {{ .sourcePath }} from node {{ .source }} to node {{ .dest }}:{{ .destPath }}"
TaskRsync: "Sync"
NodeFile: "Node file"
ImageSyncTaskName: "Image sync {{ .source }} -> {{ .count }} nodes"
ImageSyncSave: "Save image {{ .image }}"
ImageSyncSaveTags: "Save image tags {{ .tags }}"
ImageSyncToNode: "Sync image {{ .image }} to node {{ .node }}"
ImageSyncLoad: "Load image {{ .image }} on node {{ .node }}"
ImageSyncClean: "Clean temporary files for image {{ .image }}"
ImageSyncNoTag: "Image {{ .image }} has no tag and will be saved by image ID"
ImageSyncFrontendTagsIgnored: "No tags were found for image {{ .image }} on the source node. Frontend tags were ignored."
ImageSyncHandlePrivateKey: "Handle image sync node private key"
ImageSyncHandlePublicKey: "Handle image sync node public key"
ImageSyncTransfer: "Start transferring image temporary file: {{ .source }}:{{ .sourcePath }} -> {{ .dest }}:{{ .destPath }}"
#cluster
ErrMasterDelete: "Tidak dapat menghapus nod utama, sila hapuskan nod perantara dahulu."
ClusterNameIsExist: "Nama kluster sudah wujud."
+19
View File
@@ -307,6 +307,25 @@ DBConnErr: "Erro: Falha ao inicializar a conexão com o banco de dados, {{ .err
SystemVersion: "versão: "
SystemMode: "modo: "
#exchange
LocalNodeIpFailed: "Failed to get master node IP. Edit the master node and add IP address and SSH authentication information."
HandlePrivateKey: "Handle node private key"
HandlePublicKey: "Handle node public key"
ExchangeFile: "Start syncing {{ .sourcePath }} from node {{ .source }} to node {{ .dest }}:{{ .destPath }}"
TaskRsync: "Sync"
NodeFile: "Node file"
ImageSyncTaskName: "Image sync {{ .source }} -> {{ .count }} nodes"
ImageSyncSave: "Save image {{ .image }}"
ImageSyncSaveTags: "Save image tags {{ .tags }}"
ImageSyncToNode: "Sync image {{ .image }} to node {{ .node }}"
ImageSyncLoad: "Load image {{ .image }} on node {{ .node }}"
ImageSyncClean: "Clean temporary files for image {{ .image }}"
ImageSyncNoTag: "Image {{ .image }} has no tag and will be saved by image ID"
ImageSyncFrontendTagsIgnored: "No tags were found for image {{ .image }} on the source node. Frontend tags were ignored."
ImageSyncHandlePrivateKey: "Handle image sync node private key"
ImageSyncHandlePublicKey: "Handle image sync node public key"
ImageSyncTransfer: "Start transferring image temporary file: {{ .source }}:{{ .sourcePath }} -> {{ .dest }}:{{ .destPath }}"
#mobile app
ErrVerifyToken: 'Erro de verificação do token, por favor, reinicie e escaneie novamente.'
ErrInvalidToken: 'Token inválido, por favor, reinicie e escaneie novamente.'
+19
View File
@@ -306,6 +306,25 @@ DBConnErr: "Ошибка: не удалось инициализировать
SystemVersion: "Версия:"
SystemMode: "Режим:"
#exchange
LocalNodeIpFailed: "Failed to get master node IP. Edit the master node and add IP address and SSH authentication information."
HandlePrivateKey: "Handle node private key"
HandlePublicKey: "Handle node public key"
ExchangeFile: "Start syncing {{ .sourcePath }} from node {{ .source }} to node {{ .dest }}:{{ .destPath }}"
TaskRsync: "Sync"
NodeFile: "Node file"
ImageSyncTaskName: "Image sync {{ .source }} -> {{ .count }} nodes"
ImageSyncSave: "Save image {{ .image }}"
ImageSyncSaveTags: "Save image tags {{ .tags }}"
ImageSyncToNode: "Sync image {{ .image }} to node {{ .node }}"
ImageSyncLoad: "Load image {{ .image }} on node {{ .node }}"
ImageSyncClean: "Clean temporary files for image {{ .image }}"
ImageSyncNoTag: "Image {{ .image }} has no tag and will be saved by image ID"
ImageSyncFrontendTagsIgnored: "No tags were found for image {{ .image }} on the source node. Frontend tags were ignored."
ImageSyncHandlePrivateKey: "Handle image sync node private key"
ImageSyncHandlePublicKey: "Handle image sync node public key"
ImageSyncTransfer: "Start transferring image temporary file: {{ .source }}:{{ .sourcePath }} -> {{ .dest }}:{{ .destPath }}"
#mobile app
ErrVerifyToken: 'шибка проверки токена, пожалуйста, сбросьте и отсканируйте снова.'
ErrInvalidToken: 'Неверный токен, пожалуйста, сбросьте и отсканируйте снова.'
+19
View File
@@ -306,6 +306,25 @@ DBConnErr: "Hata: Veritabanı bağlantısını başlatma başarısız, {{ .err }
SystemVersion: "sürüm: "
SystemMode: "mod: "
#exchange
LocalNodeIpFailed: "Failed to get master node IP. Edit the master node and add IP address and SSH authentication information."
HandlePrivateKey: "Handle node private key"
HandlePublicKey: "Handle node public key"
ExchangeFile: "Start syncing {{ .sourcePath }} from node {{ .source }} to node {{ .dest }}:{{ .destPath }}"
TaskRsync: "Sync"
NodeFile: "Node file"
ImageSyncTaskName: "Image sync {{ .source }} -> {{ .count }} nodes"
ImageSyncSave: "Save image {{ .image }}"
ImageSyncSaveTags: "Save image tags {{ .tags }}"
ImageSyncToNode: "Sync image {{ .image }} to node {{ .node }}"
ImageSyncLoad: "Load image {{ .image }} on node {{ .node }}"
ImageSyncClean: "Clean temporary files for image {{ .image }}"
ImageSyncNoTag: "Image {{ .image }} has no tag and will be saved by image ID"
ImageSyncFrontendTagsIgnored: "No tags were found for image {{ .image }} on the source node. Frontend tags were ignored."
ImageSyncHandlePrivateKey: "Handle image sync node private key"
ImageSyncHandlePublicKey: "Handle image sync node public key"
ImageSyncTransfer: "Start transferring image temporary file: {{ .source }}:{{ .sourcePath }} -> {{ .dest }}:{{ .destPath }}"
#mobile app
ErrVerifyToken: 'Token doğrulama hatası, lütfen sıfırlayın ve tekrar tarayın.'
ErrInvalidToken: 'Geçersiz token, lütfen sıfırlayın ve tekrar tarayın.'
+11
View File
@@ -316,6 +316,17 @@ HandlePublicKey: "處理節點公鑰"
ExchangeFile: "開始從 {{ .source }} 節點同步 {{ .sourcePath }} 到 {{ .dest }} 節點 {{ .destPath }}"
TaskRsync: "同步"
NodeFile: "節點檔案"
ImageSyncTaskName: "映像同步 {{ .source }} -> {{ .count }} 個節點"
ImageSyncSave: "儲存映像 {{ .image }}"
ImageSyncSaveTags: "儲存映像標籤 {{ .tags }}"
ImageSyncToNode: "同步映像 {{ .image }} 到節點 {{ .node }}"
ImageSyncLoad: "在節點 {{ .node }} 載入映像 {{ .image }}"
ImageSyncClean: "清理映像 {{ .image }} 暫存檔案"
ImageSyncNoTag: "映像 {{ .image }} 沒有標籤,將使用映像 ID 儲存"
ImageSyncFrontendTagsIgnored: "映像 {{ .image }} 在來源節點未查詢到標籤,已忽略前端傳入標籤"
ImageSyncHandlePrivateKey: "處理映像同步節點私鑰"
ImageSyncHandlePublicKey: "處理映像同步節點公鑰"
ImageSyncTransfer: "開始傳輸映像暫存檔案:{{ .source }}:{{ .sourcePath }} -> {{ .dest }}:{{ .destPath }}"
#mobile app
ErrVerifyToken: '令牌驗證錯誤,請重設後再次掃碼'
+11
View File
@@ -322,6 +322,17 @@ HandlePublicKey: "处理节点公钥"
ExchangeFile: "开始从 {{ .source }} 节点同步 {{ .sourcePath }} 到 {{ .dest }} 节点 {{ .destPath }}"
TaskRsync: "同步"
NodeFile: "节点文件"
ImageSyncTaskName: "镜像同步 {{ .source }} -> {{ .count }} 个节点"
ImageSyncSave: "保存镜像 {{ .image }}"
ImageSyncSaveTags: "保存镜像标签 {{ .tags }}"
ImageSyncToNode: "同步镜像 {{ .image }} 到节点 {{ .node }}"
ImageSyncLoad: "在节点 {{ .node }} 加载镜像 {{ .image }}"
ImageSyncClean: "清理镜像 {{ .image }} 临时文件"
ImageSyncNoTag: "镜像 {{ .image }} 没有标签,将使用镜像 ID 保存"
ImageSyncFrontendTagsIgnored: "镜像 {{ .image }} 在源节点未查询到标签,已忽略前端传入标签"
ImageSyncHandlePrivateKey: "处理镜像同步节点私钥"
ImageSyncHandlePublicKey: "处理镜像同步节点公钥"
ImageSyncTransfer: "开始传输镜像临时文件:{{ .source }}:{{ .sourcePath }} -> {{ .dest }}:{{ .destPath }}"
#mobile app
ErrVerifyToken: '令牌验证错误,请重置后再次扫码'
+2 -2
View File
@@ -78,7 +78,7 @@ func LoadMenus() string {
{ID: "113", Disabled: false, Title: "xpack.monitor.name", IsShow: true, Label: "MonitorDashboard", Path: "/xpack/monitor/dashboard", Sort: 500},
{ID: "114", Disabled: false, Title: "xpack.tamper.tamper", IsShow: true, Label: "Tamper", Path: "/xpack/tamper", Sort: 600},
{ID: "120", Disabled: false, Title: "xpack.cluster.cluster", IsShow: true, Label: "Cluster", Path: "/xpack/cluster", Sort: 700},
{ID: "115", Disabled: false, Title: "xpack.exchange.exchange", IsShow: true, Label: "FileExange", Path: "/xpack/exchange/file", Sort: 800},
{ID: "115", Disabled: false, Title: "xpack.sync.menu", IsShow: true, Label: "Sync", Path: "/xpack/sync", Sort: 800},
{ID: "117", Disabled: false, Title: "xpack.setting.setting", IsShow: true, Label: "XSetting", Path: "/xpack/setting", Sort: 900},
}},
{ID: "12", Disabled: false, Title: "menu.logs", IsShow: true, Label: "Log-Menu", Path: "/logs", Sort: 1200},
@@ -183,7 +183,7 @@ func MenuSort() []dto.MenuLabelSort {
{Label: "MonitorDashboard", Sort: 500},
{Label: "Tamper", Sort: 600},
{Label: "Cluster", Sort: 700},
{Label: "FileExange", Sort: 800},
{Label: "Sync", Sort: 800},
{Label: "XSetting", Sort: 900},
{Label: "Log-Menu", Sort: 1200},
{Label: "Setting-Menu", Sort: 1300},
+1
View File
@@ -43,6 +43,7 @@ func Init() {
migrations.AddAIBenchmarkMenu,
migrations.AddAIProxyMenu,
migrations.AddSkillsHubMenu,
migrations.UpdateXpackSyncMenu,
migrations.AddOperationLogUser,
migrations.AddLoginLogUser,
migrations.AddAlertAuditUser,
+44
View File
@@ -1164,6 +1164,50 @@ var AddSkillsHubMenu = &gormigrate.Migration{
},
}
var UpdateXpackSyncMenu = &gormigrate.Migration{
ID: "20260616-update-xpack-sync-menu",
Migrate: func(tx *gorm.DB) error {
var menuJSON string
if err := tx.Model(&model.Setting{}).Where("key = ?", "HideMenu").Pluck("value", &menuJSON).Error; err != nil {
return err
}
if menuJSON == "" {
menuJSON = helper.LoadMenus()
}
var menus []dto.ShowMenu
if err := json.Unmarshal([]byte(menuJSON), &menus); err != nil {
return tx.Model(&model.Setting{}).
Where("key = ?", "HideMenu").
Update("value", helper.LoadMenus()).Error
}
for i := range menus {
if menus[i].Label != "Xpack-Menu" {
continue
}
for j := range menus[i].Children {
if menus[i].Children[j].ID != "115" && menus[i].Children[j].Label != "FileExange" {
continue
}
menus[i].Children[j].Title = "xpack.sync.menu"
menus[i].Children[j].Label = "Sync"
menus[i].Children[j].Path = "/xpack/sync"
break
}
break
}
updatedJSON, err := json.Marshal(menus)
if err != nil {
return tx.Model(&model.Setting{}).
Where("key = ?", "HideMenu").
Update("value", helper.LoadMenus()).Error
}
return tx.Model(&model.Setting{}).Where("key = ?", "HideMenu").Update("value", string(updatedJSON)).Error
},
}
var AddUserManagementMenu = &gormigrate.Migration{
ID: "20260415-add-user-management-menu",
Migrate: func(tx *gorm.DB) error {
+1
View File
@@ -208,6 +208,7 @@ export namespace App {
dockerCompose: string;
app: App.AppDetail;
linkDB: boolean;
resourceKeys?: string[];
}
export interface AppInstalledOption {
+7 -2
View File
@@ -120,8 +120,13 @@ export const DownloadFile = (params: Container.ContainerLogInfo) => {
};
// image
export const searchImage = (params: Container.ImageSearch) => {
return http.post<ResPage<Container.ImageInfo>>(`/containers/image/search`, params);
export const searchImage = (params: Container.ImageSearch, currentNode?: string) => {
return http.post<ResPage<Container.ImageInfo>>(
`/containers/image/search`,
params,
TimeoutEnum.T_60S,
currentNode ? { CurrentNode: currentNode } : undefined,
);
};
export const listAllImage = () => {
return http.get<Array<Container.ImageInfo>>(`/containers/image/all`);
-20
View File
@@ -174,7 +174,6 @@
<OpDialog ref="opRef" @search="search" />
<TaskLog ref="taskLogRef" @close="search" />
<PushApp ref="pushAppRef" />
</template>
<script lang="ts" setup>
@@ -201,19 +200,15 @@ import TaskLog from '@/components/log/task/index.vue';
import { routerToFileWithPath } from '@/utils/router';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { mysqlArgs } from '@/views/cronjob/cronjob/helper';
import { loadOptionalComponent } from '@/extensions/optional';
const { currentNode } = useGlobalStore();
const emit = defineEmits(['close']);
const PushApp = defineAsyncComponent(() => loadOptionalComponent('/src/xpack/views/appstore/push-app/index.vue'));
const selects = ref<any>([]);
const args = ref([]);
const loading = ref();
const opRef = ref();
const taskLogRef = ref();
const pushAppRef = ref();
const data = ref();
const paginationConfig = reactive({
@@ -457,21 +452,6 @@ const buttons = [
onRecover(row);
},
},
{
label: i18n.global.t('commons.button.migrate'),
disabled: (row: any) => {
return row.size === 0 || row.status === 'Failed' || row.accountType !== 'LOCAL';
},
show: () => {
return type.value === 'app';
},
click: (row: Backup.RecordInfo) => {
pushAppRef.value.acceptParams({
appInstallID: appInstallID.value,
appBackupID: row.id,
});
},
},
{
label: i18n.global.t('commons.button.download'),
disabled: (row: any) => {
+36 -3
View File
@@ -5542,9 +5542,42 @@ const message = {
setDefaultHelper: 'The theme color scheme is about to be restored to its initial state. Continue?',
setHelper: 'The currently selected theme color scheme is about to be saved. Continue?',
},
exchange: {
exchange: 'File Exchange',
exchangeConfirm: "transfer the file/folder {1} from {0} node to {2} node's {3} directory?",
sync: {
menu: 'Resource Sync',
file: 'File Exchange',
fileSyncConfirm: 'Do you want to transfer file/folder {1} from node {0} to directory {3} on node {2}?',
image: 'Image Exchange',
imageBatchDesc: 'Select a source node and image first, then batch-transfer to multiple target nodes.',
imageBatchConfirmPrefix: 'About to transfer {0} images to {1} nodes, for a total of {2} tasks.',
cert: 'Certificate Sync',
certBatchDesc:
'After selecting a source node, sync the specified certificate into the current panel environment.',
certBatchConfirmPrefix: 'About to sync {0} certificates to {1} nodes, for a total of {2} tasks.',
app: 'Application Transfer',
appBatchDesc:
'Select the source node, application, and target nodes, then confirm resource dependencies and start the application transfer task.',
appSelectedApp: 'Selected Application:',
appPreviewTitle: 'Transfer Preview',
appPreviewDesc: 'Confirm target nodes and dependent resources before starting the application transfer.',
appDatabaseDependency: 'Database dependency',
appResourceRequired: 'Select all required linked resources first.',
appUnsupportedMultipleResources: 'Only apps with at most one linked resource can be migrated.',
appUnsupportedResourceType:
'Only apps with no resource dependency or a single MySQL, MariaDB, or PostgreSQL dependency can be migrated.',
targetTitle: 'Target nodes',
targetDesc: 'The source node is excluded automatically. Choose one or more target nodes.',
targetSearchPlaceholder: 'Search node name or address',
noTargetNodes: 'No target nodes available',
noSourceNode: 'Please select a source node first',
reviewTargets: 'Target nodes',
reviewEmpty: 'Please select resources and target nodes first',
selectSourceFirst: 'Please select a source node first.',
selectResourceFirst: 'Please select resources to sync first.',
selectTargetFirst: 'Please select at least one target node.',
submitSuccess: 'Sync task submitted.',
},
cluster: {
cluster: 'Application High Availability',
+40 -3
View File
@@ -5602,9 +5602,46 @@ const message = {
setDefaultHelper: 'La paleta de colores volverá al estado inicial. ¿Continuar?',
setHelper: 'Se guardará la paleta actual como tema. ¿Continuar?',
},
exchange: {
exchange: 'Intercambio de archivos',
exchangeConfirm: '¿Quieres transferir {1} de {0} a {2}:{3}?',
sync: {
menu: 'Sincronización de Recursos',
file: 'Transferencia de archivos',
fileSyncConfirm: '¿Transferir el archivo/carpeta {1} del nodo {0} al directorio {3} del nodo {2}?',
image: 'Transferencia de imágenes',
imageBatchDesc:
'Seleccione primero el nodo de origen y las imágenes de origen, luego elija los nodos de destino para reenviar.',
imageBatchConfirmPrefix: 'Se enviarán {0} imágenes a {1} nodos, creando {2} tareas.',
cert: 'Sincronización de certificados',
certBatchDesc:
'Después de seleccionar el nodo de origen, los certificados especificados pueden sincronizarse con el entorno actual del panel.',
certBatchConfirmPrefix: 'Se sincronizarán {0} certificados a {1} nodos, creando {2} tareas.',
app: 'Transferencia de Aplicaciones',
appBatchDesc:
'Seleccione el nodo de origen, la aplicación y los nodos de destino, luego confirme las dependencias de recursos y ejecute la tarea de transferencia de aplicaciones.',
appSelectedApp: 'Aplicación seleccionada:',
appPreviewTitle: 'Vista previa de transferencia',
appPreviewDesc:
'Confirme los nodos de destino y los recursos dependientes antes de iniciar la transferencia de aplicaciones.',
appDatabaseDependency: 'Dependencia de base de datos',
appResourceRequired: 'Seleccione primero todos los recursos asociados requeridos.',
appUnsupportedMultipleResources:
'Actualmente solo se admite la migración de aplicaciones asociadas con un máximo de un recurso.',
appUnsupportedResourceType:
'Actualmente solo se admite la migración de aplicaciones sin dependencias de recursos o que dependan únicamente de MySQL, MariaDB o PostgreSQL.',
targetTitle: 'Nodos de destino',
targetDesc: 'El nodo de origen se excluye automáticamente. Puede seleccionar uno o más nodos de destino.',
targetSearchPlaceholder: 'Buscar nombre o dirección del nodo',
noTargetNodes: 'No hay nodos de destino disponibles',
noSourceNode: 'Seleccione primero un nodo de origen',
reviewTargets: 'Nodos de destino',
reviewEmpty: 'Seleccione primero los recursos y los nodos de destino',
selectSourceFirst: 'Seleccione primero un nodo de origen.',
selectResourceFirst: 'Seleccione primero un recurso para sincronizar.',
selectTargetFirst: 'Seleccione al menos un nodo de destino.',
submitSuccess: 'La tarea de sincronización se ha enviado correctamente.',
},
cluster: {
cluster: 'Alta disponibilidad de aplicaciones',
+37 -3
View File
@@ -5577,9 +5577,43 @@ const message = {
setDefaultHelper: 'テーマカラーを初期状態に戻そうとしています。続行しますか?',
setHelper: '現在選択されているテーマカラーを保存しようとしています。続行しますか?',
},
exchange: {
exchange: 'ファイル交換',
exchangeConfirm: '{0} ノードのファイル/フォルダ {1} を {2} ノードの {3} ディレクトリに転送しますか?',
sync: {
menu: 'リソース同期',
file: 'ファイル転送',
fileSyncConfirm: 'ノード {0} のファイル/フォルダ {1} をノード {2} のディレクトリ {3} に転送しますか?',
image: 'イメージ転送',
imageBatchDesc: 'まずソースノードとソースイメージを選択し、その後転送先ノードを選択してください。',
imageBatchConfirmPrefix:
'{0} 個のイメージを {1} 個のノードへ転送します。合計 {2} 件のタスクが作成されます。',
cert: '証明書同期',
certBatchDesc: 'ソースノードを選択すると、指定した証明書を現在のパネル環境へ同期できます。',
certBatchConfirmPrefix: '{0} 件の証明書を {1} 個のノードへ同期します。合計 {2} 件のタスクが作成されます。',
app: 'アプリ転送',
appBatchDesc:
'ソースノード、アプリケーション、ターゲットノードを選択し、リソース依存関係を確認してアプリ転送タスクを実行します。',
appSelectedApp: '選択したアプリ:',
appPreviewTitle: '転送プレビュー',
appPreviewDesc: 'ターゲットノードと依存リソースを確認してからアプリ転送を実行してください。',
appDatabaseDependency: 'データベース依存関係',
appResourceRequired: '必要な関連リソースをすべて選択してください。',
appUnsupportedMultipleResources: '現在、関連リソースが最大1つのアプリのみ移行できます。',
appUnsupportedResourceType:
'現在、リソース依存がないアプリ、または MySQL・MariaDB・PostgreSQL のみを利用するアプリのみ移行できます。',
targetTitle: 'ターゲットノード',
targetDesc: 'ソースノードは自動的に除外されます。1つ以上のターゲットノードを選択できます。',
targetSearchPlaceholder: 'ノード名またはアドレスを検索',
noTargetNodes: '利用可能なターゲットノードがありません',
noSourceNode: 'まずソースノードを選択してください',
reviewTargets: 'ターゲットノード',
reviewEmpty: 'まずリソースとターゲットノードを選択してください',
selectSourceFirst: 'まずソースノードを選択してください。',
selectResourceFirst: 'まず同期するリソースを選択してください。',
selectTargetFirst: '少なくとも1つのターゲットノードを選択してください。',
submitSuccess: '同期タスクが送信されました。',
},
cluster: {
cluster: 'アプリケーションの高可用性',
+37 -3
View File
@@ -5462,9 +5462,43 @@ const message = {
setDefaultHelper: '테마 색상 스킴이 초기 상태로 복원됩니다. 계속하시겠습니까?',
setHelper: '현재 선택한 테마 색상 스킴이 저장됩니다. 계속하시겠습니까?',
},
exchange: {
exchange: '파일 교환',
exchangeConfirm: '{0} 노드의 파일/폴더 {1}을(를) {2} 노드의 {3} 디렉토리로 전송하시겠습니까?',
sync: {
menu: '리소스 동기화',
file: '파일 전송',
fileSyncConfirm: '{0} 노드의 파일/폴더 {1}을(를) {2} 노드의 디렉터리 {3}(으)로 전송하시겠습니까?',
image: '이미지 전송',
imageBatchDesc: '먼저 원본 노드와 원본 이미지를 선택한 후 대상 노드를 선택하여 전달합니다.',
imageBatchConfirmPrefix: '{0}개의 이미지를 {1}개 노드로 전달합니다. 총 {2}개의 작업이 생성됩니다.',
cert: '인증서 동기화',
certBatchDesc: '원본 노드를 선택한 후 지정된 인증서를 현재 패널 환경으로 동기화할 수 있습니다.',
certBatchConfirmPrefix: '{0}개의 인증서를 {1}개 노드로 동기화합니다. 총 {2}개의 작업이 생성됩니다.',
app: '애플리케이션 전송',
appBatchDesc:
'원본 노드, 애플리케이션 및 대상 노드를 선택한 후 리소스 종속성을 확인하고 애플리케이션 전송 작업을 실행합니다.',
appSelectedApp: '선택한 애플리케이션:',
appPreviewTitle: '전송 미리보기',
appPreviewDesc: '대상 노드와 종속 리소스를 확인한 후 애플리케이션 전송을 실행하세요.',
appDatabaseDependency: '데이터베이스 종속성',
appResourceRequired: '필수 연관 리소스를 모두 먼저 선택하세요.',
appUnsupportedMultipleResources:
'현재 최대 하나의 리소스만 연결된 애플리케이션만 마이그레이션할 수 있습니다.',
appUnsupportedResourceType:
'현재 리소스 종속성이 없거나 MySQL, MariaDB, PostgreSQL에만 의존하는 애플리케이션만 마이그레이션할 수 있습니다.',
targetTitle: '대상 노드',
targetDesc: '원본 노드는 자동으로 제외됩니다. 하나 이상의 대상 노드를 선택할 수 있습니다.',
targetSearchPlaceholder: '노드 이름 또는 주소 검색',
noTargetNodes: '선택 가능한 대상 노드가 없습니다',
noSourceNode: '먼저 원본 노드를 선택하세요',
reviewTargets: '대상 노드',
reviewEmpty: '먼저 리소스와 대상 노드를 선택하세요',
selectSourceFirst: '먼저 원본 노드를 선택하세요.',
selectResourceFirst: '먼저 동기화할 리소스를 선택하세요.',
selectTargetFirst: '최소 하나의 대상 노드를 선택하세요.',
submitSuccess: '동기화 작업이 제출되었습니다.',
},
cluster: {
cluster: '애플리케이션 고가용성',
+40 -3
View File
@@ -5649,9 +5649,46 @@ const message = {
setDefaultHelper: 'Skema warna tema akan dipulihkan ke keadaan asalnya. Adakah anda ingin meneruskan?',
setHelper: 'Skema warna tema yang dipilih sekarang akan disimpan. Adakah anda ingin meneruskan?',
},
exchange: {
exchange: 'Pertukaran Fail',
exchangeConfirm: 'Adakah anda mahu memindahkan fail/folder {1} dari node {0} ke direktori {3} node {2}?',
sync: {
menu: 'Penyegerakan Sumber',
file: 'Pemindahan Fail',
fileSyncConfirm: 'Pindahkan fail/folder {1} dari nod {0} ke direktori {3} pada nod {2}?',
image: 'Pemindahan Imej',
imageBatchDesc:
'Pilih nod sumber dan imej sumber terlebih dahulu, kemudian pilih nod sasaran untuk pemindahan.',
imageBatchConfirmPrefix: 'Akan memindahkan {0} imej ke {1} nod, dengan jumlah {2} tugasan.',
cert: 'Penyegerakan Sijil',
certBatchDesc:
'Selepas memilih nod sumber, sijil yang ditentukan boleh disegerakkan ke persekitaran panel semasa.',
certBatchConfirmPrefix: 'Akan menyegerakkan {0} sijil ke {1} nod, dengan jumlah {2} tugasan.',
app: 'Pemindahan Aplikasi',
appBatchDesc:
'Pilih nod sumber, aplikasi dan nod sasaran, kemudian sahkan kebergantungan sumber dan jalankan tugas pemindahan aplikasi.',
appSelectedApp: 'Aplikasi Dipilih:',
appPreviewTitle: 'Pratonton Pemindahan',
appPreviewDesc: 'Sahkan nod sasaran dan sumber bergantung sebelum memulakan pemindahan aplikasi.',
appDatabaseDependency: 'Kebergantungan Pangkalan Data',
appResourceRequired: 'Sila pilih semua sumber berkaitan yang diperlukan terlebih dahulu.',
appUnsupportedMultipleResources:
'Pada masa ini, hanya aplikasi yang dikaitkan dengan maksimum satu sumber boleh dipindahkan.',
appUnsupportedResourceType:
'Pada masa ini, hanya aplikasi tanpa kebergantungan sumber atau yang bergantung pada MySQL, MariaDB atau PostgreSQL sahaja boleh dipindahkan.',
targetTitle: 'Nod Sasaran',
targetDesc:
'Nod sumber akan dikecualikan secara automatik. Anda boleh memilih satu atau lebih nod sasaran.',
targetSearchPlaceholder: 'Cari nama atau alamat nod',
noTargetNodes: 'Tiada nod sasaran tersedia',
noSourceNode: 'Sila pilih nod sumber terlebih dahulu',
reviewTargets: 'Nod Sasaran',
reviewEmpty: 'Sila pilih sumber dan nod sasaran terlebih dahulu',
selectSourceFirst: 'Sila pilih nod sumber terlebih dahulu.',
selectResourceFirst: 'Sila pilih sumber untuk disegerakkan terlebih dahulu.',
selectTargetFirst: 'Sila pilih sekurang-kurangnya satu nod sasaran.',
submitSuccess: 'Tugas penyegerakan berjaya dihantar.',
},
cluster: {
cluster: 'Aplikasi Tinggi Ketersediaan',
+40 -3
View File
@@ -5796,9 +5796,46 @@ const message = {
setDefaultHelper: 'O esquema de cores do tema será restaurado para o estado inicial. Deseja continuar?',
setHelper: 'O esquema de cores do tema selecionado será salvo. Deseja continuar?',
},
exchange: {
exchange: 'Troca de Arquivos',
exchangeConfirm: 'Deseja transferir o arquivo/pasta {1} do nó {0} para o diretório {3} do nó {2}?',
sync: {
menu: 'Sincronização de Recursos',
file: 'Transferência de Arquivos',
fileSyncConfirm: 'Transferir o arquivo/pasta {1} do nó {0} para o diretório {3} no nó {2}?',
image: 'Transferência de Imagens',
imageBatchDesc:
'Selecione primeiro o nó de origem e as imagens de origem e, em seguida, escolha os nós de destino para encaminhamento.',
imageBatchConfirmPrefix: 'Serão encaminhadas {0} imagens para {1} nós, totalizando {2} tarefas.',
cert: 'Sincronização de Certificados',
certBatchDesc:
'Após selecionar o nó de origem, os certificados especificados podem ser sincronizados com o ambiente atual do painel.',
certBatchConfirmPrefix: 'Serão sincronizados {0} certificados para {1} nós, totalizando {2} tarefas.',
app: 'Transferência de Aplicativos',
appBatchDesc:
'Selecione o nó de origem, o aplicativo e os nós de destino, confirme as dependências de recursos e execute a tarefa de transferência de aplicativos.',
appSelectedApp: 'Aplicativo Selecionado:',
appPreviewTitle: 'Prévia da Transferência',
appPreviewDesc:
'Confirme os nós de destino e os recursos dependentes antes de iniciar a transferência do aplicativo.',
appDatabaseDependency: 'Dependência de Banco de Dados',
appResourceRequired: 'Selecione primeiro todos os recursos associados obrigatórios.',
appUnsupportedMultipleResources:
'Atualmente, apenas aplicativos associados a no máximo um recurso podem ser migrados.',
appUnsupportedResourceType:
'Atualmente, apenas aplicativos sem dependências de recursos ou dependentes apenas de MySQL, MariaDB ou PostgreSQL podem ser migrados.',
targetTitle: 'Nós de Destino',
targetDesc: 'O nó de origem será excluído automaticamente. Você pode selecionar um ou mais nós de destino.',
targetSearchPlaceholder: 'Pesquisar nome ou endereço do nó',
noTargetNodes: 'Nenhum nó de destino disponível',
noSourceNode: 'Selecione primeiro um nó de origem',
reviewTargets: 'Nós de Destino',
reviewEmpty: 'Selecione primeiro os recursos e os nós de destino',
selectSourceFirst: 'Selecione primeiro um nó de origem.',
selectResourceFirst: 'Selecione primeiro um recurso para sincronizar.',
selectTargetFirst: 'Selecione pelo menos um nó de destino.',
submitSuccess: 'Tarefa de sincronização enviada com sucesso.',
},
cluster: {
cluster: 'Alta Disponibilidade de Aplicações',
+39 -3
View File
@@ -5648,9 +5648,45 @@ const message = {
setDefaultHelper: 'Цветовая схема темы будет восстановлена до исходного состояния. Вы хотите продолжить?',
setHelper: 'Выбранная в данный момент цветовая схема темы будет сохранена. Вы хотите продолжить?',
},
exchange: {
exchange: 'Обмен файлами',
exchangeConfirm: 'Хотите перенести файл/папку {1} с узла {0} в каталог {3} узла {2}?',
sync: {
menu: 'Синхронизация ресурсов',
file: 'Передача файлов',
fileSyncConfirm: 'Передать файл/папку {1} с узла {0} в каталог {3} на узле {2}?',
image: 'Передача образов',
imageBatchDesc:
'Сначала выберите исходный узел и исходные образы, затем выберите целевые узлы для передачи.',
imageBatchConfirmPrefix: 'Будет передано {0} образов на {1} узлов, всего {2} задач.',
cert: 'Синхронизация сертификатов',
certBatchDesc:
'После выбора исходного узла указанные сертификаты можно синхронизировать с текущей средой панели.',
certBatchConfirmPrefix: 'Будет синхронизировано {0} сертификатов на {1} узлов, всего {2} задач.',
app: 'Передача приложений',
appBatchDesc:
'Выберите исходный узел, приложение и целевые узлы, подтвердите зависимости ресурсов и запустите задачу передачи приложения.',
appSelectedApp: 'Выбранное приложение:',
appPreviewTitle: 'Предварительный просмотр передачи',
appPreviewDesc: 'Подтвердите целевые узлы и зависимые ресурсы перед запуском передачи приложения.',
appDatabaseDependency: 'Зависимость базы данных',
appResourceRequired: 'Сначала выберите все необходимые связанные ресурсы.',
appUnsupportedMultipleResources:
'В настоящее время поддерживается миграция только приложений, связанных максимум с одним ресурсом.',
appUnsupportedResourceType:
'В настоящее время поддерживается миграция только приложений без зависимостей ресурсов или зависящих только от MySQL, MariaDB или PostgreSQL.',
targetTitle: 'Целевые узлы',
targetDesc: 'Исходный узел будет автоматически исключён. Можно выбрать один или несколько целевых узлов.',
targetSearchPlaceholder: 'Поиск по имени или адресу узла',
noTargetNodes: 'Нет доступных целевых узлов',
noSourceNode: 'Сначала выберите исходный узел',
reviewTargets: 'Целевые узлы',
reviewEmpty: 'Сначала выберите ресурсы и целевые узлы',
selectSourceFirst: 'Сначала выберите исходный узел.',
selectResourceFirst: 'Сначала выберите ресурс для синхронизации.',
selectTargetFirst: 'Выберите хотя бы один целевой узел.',
submitSuccess: 'Задача синхронизации успешно отправлена.',
},
cluster: {
cluster: 'Высокая доступность приложений',
+37 -3
View File
@@ -5639,9 +5639,43 @@ const message = {
setDefaultHelper: 'Tema renk şeması başlangıç durumuna geri yüklenecek. Devam etmek istiyor musunuz?',
setHelper: 'Seçilen tema renk şeması kaydedilecek. Devam etmek istiyor musunuz?',
},
exchange: {
exchange: 'Dosya Değişimi',
exchangeConfirm: '{0} düğümünden {1} dosya/klasörünü {2} düğümünün {3} dizinine aktarmak istiyor musunuz?',
sync: {
menu: 'Kaynak Senkronizasyonu',
file: 'Dosya Aktarımı',
fileSyncConfirm: '{0} düğümündeki {1} dosya/klasörü {2} düğümündeki {3} dizinine aktarmak istiyor musunuz?',
image: 'İmaj Aktarımı',
imageBatchDesc: 'Önce kaynak düğümü ve kaynak imajları seçin, ardından iletim için hedef düğümleri seçin.',
imageBatchConfirmPrefix: '{0} imaj {1} düğüme aktarılacak, toplam {2} görev oluşturulacak.',
cert: 'Sertifika Senkronizasyonu',
certBatchDesc:
'Kaynak düğüm seçildikten sonra belirtilen sertifikalar mevcut panel ortamına senkronize edilebilir.',
certBatchConfirmPrefix: '{0} sertifika {1} düğüme senkronize edilecek, toplam {2} görev oluşturulacak.',
app: 'Uygulama Aktarımı',
appBatchDesc:
'Kaynak düğümü, uygulamayı ve hedef düğümleri seçin, ardından kaynak bağımlılıklarını doğrulayarak uygulama aktarım görevini başlatın.',
appSelectedApp: 'Seçilen Uygulama:',
appPreviewTitle: 'Aktarım Önizlemesi',
appPreviewDesc: 'Uygulama aktarımını başlatmadan önce hedef düğümleri ve bağımlı kaynakları doğrulayın.',
appDatabaseDependency: 'Veritabanı Bağımlılığı',
appResourceRequired: 'Lütfen önce gerekli tüm ilişkili kaynakları seçin.',
appUnsupportedMultipleResources: 'Şu anda yalnızca en fazla bir kaynakla ilişkili uygulamalar taşınabilir.',
appUnsupportedResourceType:
'Şu anda yalnızca kaynak bağımlılığı olmayan veya yalnızca MySQL, MariaDB ya da PostgreSQL kullanan uygulamalar taşınabilir.',
targetTitle: 'Hedef Düğümler',
targetDesc: 'Kaynak düğüm otomatik olarak hariç tutulur. Bir veya daha fazla hedef düğüm seçebilirsiniz.',
targetSearchPlaceholder: 'Düğüm adı veya adres ara',
noTargetNodes: 'Kullanılabilir hedef düğüm yok',
noSourceNode: 'Lütfen önce bir kaynak düğüm seçin',
reviewTargets: 'Hedef Düğümler',
reviewEmpty: 'Lütfen önce kaynakları ve hedef düğümleri seçin',
selectSourceFirst: 'Lütfen önce bir kaynak düğüm seçin.',
selectResourceFirst: 'Lütfen önce senkronize edilecek kaynağı seçin.',
selectTargetFirst: 'Lütfen en az bir hedef düğüm seçin.',
submitSuccess: 'Senkronizasyon görevi başarıyla gönderildi.',
},
cluster: {
cluster: 'Высокая доступность приложений',
+34 -3
View File
@@ -5181,9 +5181,40 @@ const message = {
qrCodeExpired: '重新整理時間',
apiLeakageHelper: '請勿洩露二維碼,確保僅在受信任的環境中使用',
},
exchange: {
exchange: '檔案對傳',
exchangeConfirm: '是否將 {0} 節點檔案/資料夾 {1} 傳輸到 {2} 節點 {3} 目錄?',
sync: {
menu: '資源同步',
file: '檔案對傳',
fileSyncConfirm: '是否將 {0} 節點檔案/資料夾 {1} 傳輸到 {2} 節點 {3} 目錄?',
image: '映像對傳',
imageBatchDesc: '先選擇來源節點和來源映像,再批次選擇目標節點進行轉發。',
imageBatchConfirmPrefix: '即將把 {0} 張映像轉發到 {1} 個節點,共 {2} 個任務。',
cert: '證書同步',
certBatchDesc: '選擇來源節點後,可將指定證書同步到目前面板環境。',
certBatchConfirmPrefix: '即將把 {0} 張證書同步到 {1} 個節點,共 {2} 個任務。',
app: '應用對傳',
appBatchDesc: '選擇來源節點、單個應用和目標節點後,確認資源依賴並執行應用對傳任務。',
appSelectedApp: '已選應用:',
appPreviewTitle: '對傳預覽',
appPreviewDesc: '確認目標節點和依賴資源後執行應用對傳。',
appDatabaseDependency: '資料庫依賴',
appResourceRequired: '請先選擇所有必需的關聯資源。',
appUnsupportedMultipleResources: '目前僅支援遷移最多關聯一個資源的應用。',
appUnsupportedResourceType: '目前僅支援遷移無資源依賴或僅依賴 MySQL、MariaDB、PostgreSQL 的應用。',
targetTitle: '目標節點',
targetDesc: '來源節點會自動排除,可選擇一個或多個目標節點。',
targetSearchPlaceholder: '搜尋節點名稱或位址',
noTargetNodes: '沒有可選目標節點',
noSourceNode: '請先選擇來源節點',
reviewTargets: '目標節點',
reviewEmpty: '請先選擇資源和目標節點',
selectSourceFirst: '請先選擇來源節點。',
selectResourceFirst: '請先選擇要同步的資源。',
selectTargetFirst: '請先選擇至少一個目標節點。',
submitSuccess: '同步任務已提交。',
},
cluster: {
cluster: '應用高可用',
+34 -3
View File
@@ -5157,9 +5157,40 @@ const message = {
qrCodeExpired: '刷新时间',
apiLeakageHelper: '请勿泄露二维码,确保仅在受信任的环境中使用',
},
exchange: {
exchange: '文件对传',
exchangeConfirm: '是否将 {0} 节点文件/文件夹 {1} 传输到 {2} 节点 {3} 目录?',
sync: {
menu: '资源同步',
file: '文件对传',
fileSyncConfirm: '是否将 {0} 节点文件/文件夹 {1} 传输到 {2} 节点 {3} 目录?',
image: '镜像对传',
imageBatchDesc: '先选择源节点和源镜像,再批量选择目标节点进行转发。',
imageBatchConfirmPrefix: '即将把 {0} 张镜像转发到 {1} 个节点,共 {2} 个任务。',
cert: '证书同步',
certBatchDesc: '选择源节点后,可将指定证书同步到当前面板环境。',
certBatchConfirmPrefix: '即将把 {0} 张证书同步到 {1} 个节点,共 {2} 个任务。',
app: '应用对传',
appBatchDesc: '选择源节点、单个应用和目标节点后,确认资源依赖并执行应用对传任务。',
appSelectedApp: '已选应用:',
appPreviewTitle: '对传预览',
appPreviewDesc: '确认目标节点和依赖资源后执行应用对传。',
appDatabaseDependency: '数据库依赖',
appResourceRequired: '请先选择所有必需的关联资源。',
appUnsupportedMultipleResources: '当前仅支持迁移最多关联一个资源的应用。',
appUnsupportedResourceType: '当前仅支持迁移无资源依赖或仅依赖 MySQL、MariaDB、PostgreSQL 的应用。',
targetTitle: '目标节点',
targetDesc: '源节点会自动排除,可选择一个或多个目标节点。',
targetSearchPlaceholder: '搜索节点名称或地址',
noTargetNodes: '没有可选目标节点',
noSourceNode: '请先选择源节点',
reviewTargets: '目标节点',
reviewEmpty: '请先选择资源和目标节点',
selectSourceFirst: '请先选择源节点。',
selectResourceFirst: '请先选择要同步的资源。',
selectTargetFirst: '请先选择至少一个目标节点。',
submitSuccess: '同步任务已提交。',
},
cluster: {
cluster: '应用高可用',