From 46af88e1d0d71c39ff8573a0a11427aefd09c7ce Mon Sep 17 00:00:00 2001 From: ssongliu Date: Wed, 17 Jun 2026 15:41:09 +0800 Subject: [PATCH] feat: support app and image synchronization (#13067) --- agent/app/api/v2/backup.go | 7 +++- agent/app/dto/backup.go | 17 ++++----- agent/app/dto/response/app.go | 1 + agent/app/service/app_utils.go | 40 ++++++++++++++------- agent/app/service/backup_app.go | 25 ++++++++++++-- agent/cmd/server/docs/x-log.json | 11 +----- agent/i18n/lang/en.yaml | 1 + agent/i18n/lang/es-ES.yaml | 1 + agent/i18n/lang/ja.yaml | 1 + agent/i18n/lang/ko.yaml | 1 + agent/i18n/lang/ms.yaml | 1 + agent/i18n/lang/pt-BR.yaml | 1 + agent/i18n/lang/ru.yaml | 1 + agent/i18n/lang/tr.yaml | 1 + agent/i18n/lang/zh-Hant.yaml | 1 + agent/i18n/lang/zh.yaml | 1 + core/cmd/server/docs/x-log.json | 11 +----- core/constant/common.go | 5 ++- core/i18n/lang/en.yaml | 19 ++++++++++ core/i18n/lang/es-ES.yaml | 19 ++++++++++ core/i18n/lang/ja.yaml | 19 ++++++++++ core/i18n/lang/ko.yaml | 19 ++++++++++ core/i18n/lang/ms.yaml | 19 ++++++++++ core/i18n/lang/pt-BR.yaml | 19 ++++++++++ core/i18n/lang/ru.yaml | 19 ++++++++++ core/i18n/lang/tr.yaml | 19 ++++++++++ core/i18n/lang/zh-Hant.yaml | 11 ++++++ core/i18n/lang/zh.yaml | 11 ++++++ core/init/migration/helper/menu.go | 4 +-- core/init/migration/migrate.go | 1 + core/init/migration/migrations/init.go | 44 ++++++++++++++++++++++++ frontend/src/api/interface/app.ts | 1 + frontend/src/api/modules/container.ts | 9 +++-- frontend/src/components/backup/index.vue | 20 ----------- frontend/src/lang/modules/en.ts | 39 +++++++++++++++++++-- frontend/src/lang/modules/es-es.ts | 43 +++++++++++++++++++++-- frontend/src/lang/modules/ja.ts | 40 +++++++++++++++++++-- frontend/src/lang/modules/ko.ts | 40 +++++++++++++++++++-- frontend/src/lang/modules/ms.ts | 43 +++++++++++++++++++++-- frontend/src/lang/modules/pt-br.ts | 43 +++++++++++++++++++++-- frontend/src/lang/modules/ru.ts | 42 ++++++++++++++++++++-- frontend/src/lang/modules/tr.ts | 40 +++++++++++++++++++-- frontend/src/lang/modules/zh-Hant.ts | 37 ++++++++++++++++++-- frontend/src/lang/modules/zh.ts | 37 ++++++++++++++++++-- 44 files changed, 684 insertions(+), 100 deletions(-) diff --git a/agent/app/api/v2/backup.go b/agent/app/api/v2/backup.go index f7e96724a..4cb19e9f1 100644 --- a/agent/app/api/v2/backup.go +++ b/agent/app/api/v2/backup.go @@ -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) diff --git a/agent/app/dto/backup.go b/agent/app/dto/backup.go index fd4e4469e..e209710af 100644 --- a/agent/app/dto/backup.go +++ b/agent/app/dto/backup.go @@ -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"` } diff --git a/agent/app/dto/response/app.go b/agent/app/dto/response/app.go index dc5e66967..239d3e0e1 100644 --- a/agent/app/dto/response/app.go +++ b/agent/app/dto/response/app.go @@ -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"` } diff --git a/agent/app/service/app_utils.go b/agent/app/service/app_utils.go index b5b3f5ad8..66869f78e 100644 --- a/agent/app/service/app_utils.go +++ b/agent/app/service/app_utils.go @@ -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 } diff --git a/agent/app/service/backup_app.go b/agent/app/service/backup_app.go index 06ed5bcff..19323f1af 100644 --- a/agent/app/service/backup_app.go +++ b/agent/app/service/backup_app.go @@ -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 } diff --git a/agent/cmd/server/docs/x-log.json b/agent/cmd/server/docs/x-log.json index b4ac76989..f54046ade 100644 --- a/agent/cmd/server/docs/x-log.json +++ b/agent/cmd/server/docs/x-log.json @@ -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" } -} \ No newline at end of file +} diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index f3b0dbd10..05e1846ae 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -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' diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index 8fa23c76f..3bd9545fb 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -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' diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 1a3ac5a2c..0db5186c4 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -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: '異常な状態です。ログを確認してください' diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 0c04a83b1..910403464 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -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: '비정상적인 상태입니다. 로그를 확인해 주세요.' diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index d26c77439..cd935d6db 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -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' diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 70b1798a9..7fa6ed8c4 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -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' diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index aecdb256b..f2eb0d5a0 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -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: 'Ненормальное состояние, проверьте журнал' diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index 6fd8dd047..33fd80def 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -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' diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index e8e1c270a..a6787772c 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -107,6 +107,7 @@ ErrContainerMsg: '{{ .name }} 容器異常,請在容器頁面上檢視日誌' ErrAppBackup: '{{ .name }} 應用備份失敗 {{ .err }}' ErrVersionTooLow: '目前 1Panel 版本過低,無法更新應用程式商店,請升級版本之後操作' ErrAppNameExist: '應用程式名稱已存在' +ErrAppInstallNameExist: '應用程式安裝名稱 {{ .name }} 已存在' AppStoreIsSyncing: '應用程式商店正在同步中,請稍後重試' ErrGetCompose: 'docker-compose.yml 檔案取得失敗!{{ .detail }}' ErrAppWarn: '狀態異常,請檢視日誌' diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 395e5f803..3281b364a 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -108,6 +108,7 @@ ErrContainerMsg: '{{ .name }} 容器异常,具体请在容器页面查看日 ErrAppBackup: '{{ .name }} 应用备份失败 {{ .err }}' ErrVersionTooLow: '当前 1Panel 版本过低,无法更新应用商店,请升级版本之后操作' ErrAppNameExist: '应用名称已存在' +ErrAppInstallNameExist: '应用安装名称 {{ .name }} 已存在' AppStoreIsSyncing: '应用商店同步中,请稍后重试' ErrGetCompose: "docker-compose.yml 文件获取失败!{{ .detail }}" ErrAppWarn: "状态异常,请查看日志" diff --git a/core/cmd/server/docs/x-log.json b/core/cmd/server/docs/x-log.json index b4ac76989..f54046ade 100644 --- a/core/cmd/server/docs/x-log.json +++ b/core/cmd/server/docs/x-log.json @@ -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" } -} \ No newline at end of file +} diff --git a/core/constant/common.go b/core/constant/common.go index aab15dc3c..363409ff2 100644 --- a/core/constant/common.go +++ b/core/constant/common.go @@ -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": {}, diff --git a/core/i18n/lang/en.yaml b/core/i18n/lang/en.yaml index 099ec2c7e..2123a2df5 100644 --- a/core/i18n/lang/en.yaml +++ b/core/i18n/lang/en.yaml @@ -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.' diff --git a/core/i18n/lang/es-ES.yaml b/core/i18n/lang/es-ES.yaml index 39d4f27dc..e28595365 100644 --- a/core/i18n/lang/es-ES.yaml +++ b/core/i18n/lang/es-ES.yaml @@ -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.' diff --git a/core/i18n/lang/ja.yaml b/core/i18n/lang/ja.yaml index 0d772855a..f8d946d80 100644 --- a/core/i18n/lang/ja.yaml +++ b/core/i18n/lang/ja.yaml @@ -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コードをスキャンしてください。' diff --git a/core/i18n/lang/ko.yaml b/core/i18n/lang/ko.yaml index 2d71a3823..013a51206 100644 --- a/core/i18n/lang/ko.yaml +++ b/core/i18n/lang/ko.yaml @@ -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: '유효하지 않은 토큰입니다. 재설정 후 다시 스캔해 주세요.' diff --git a/core/i18n/lang/ms.yaml b/core/i18n/lang/ms.yaml index 48f0e647a..89713525c 100644 --- a/core/i18n/lang/ms.yaml +++ b/core/i18n/lang/ms.yaml @@ -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." diff --git a/core/i18n/lang/pt-BR.yaml b/core/i18n/lang/pt-BR.yaml index 6a3449440..e03e735ab 100644 --- a/core/i18n/lang/pt-BR.yaml +++ b/core/i18n/lang/pt-BR.yaml @@ -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.' diff --git a/core/i18n/lang/ru.yaml b/core/i18n/lang/ru.yaml index 99d49e0a6..915fcf3d4 100644 --- a/core/i18n/lang/ru.yaml +++ b/core/i18n/lang/ru.yaml @@ -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: 'Неверный токен, пожалуйста, сбросьте и отсканируйте снова.' diff --git a/core/i18n/lang/tr.yaml b/core/i18n/lang/tr.yaml index 3a14a99da..a91359fe3 100644 --- a/core/i18n/lang/tr.yaml +++ b/core/i18n/lang/tr.yaml @@ -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.' diff --git a/core/i18n/lang/zh-Hant.yaml b/core/i18n/lang/zh-Hant.yaml index 81b0bf445..721abb445 100644 --- a/core/i18n/lang/zh-Hant.yaml +++ b/core/i18n/lang/zh-Hant.yaml @@ -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: '令牌驗證錯誤,請重設後再次掃碼' diff --git a/core/i18n/lang/zh.yaml b/core/i18n/lang/zh.yaml index 339778f7a..3e8300c73 100644 --- a/core/i18n/lang/zh.yaml +++ b/core/i18n/lang/zh.yaml @@ -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: '令牌验证错误,请重置后再次扫码' diff --git a/core/init/migration/helper/menu.go b/core/init/migration/helper/menu.go index 1b484398d..f6de356e5 100644 --- a/core/init/migration/helper/menu.go +++ b/core/init/migration/helper/menu.go @@ -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}, diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go index f84785c93..a3c9aaa32 100644 --- a/core/init/migration/migrate.go +++ b/core/init/migration/migrate.go @@ -43,6 +43,7 @@ func Init() { migrations.AddAIBenchmarkMenu, migrations.AddAIProxyMenu, migrations.AddSkillsHubMenu, + migrations.UpdateXpackSyncMenu, migrations.AddOperationLogUser, migrations.AddLoginLogUser, migrations.AddAlertAuditUser, diff --git a/core/init/migration/migrations/init.go b/core/init/migration/migrations/init.go index 999be087f..a85c64c46 100644 --- a/core/init/migration/migrations/init.go +++ b/core/init/migration/migrations/init.go @@ -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 { diff --git a/frontend/src/api/interface/app.ts b/frontend/src/api/interface/app.ts index d3aaaed19..251527ae1 100644 --- a/frontend/src/api/interface/app.ts +++ b/frontend/src/api/interface/app.ts @@ -208,6 +208,7 @@ export namespace App { dockerCompose: string; app: App.AppDetail; linkDB: boolean; + resourceKeys?: string[]; } export interface AppInstalledOption { diff --git a/frontend/src/api/modules/container.ts b/frontend/src/api/modules/container.ts index 0cffc7e93..b1bbcf5be 100644 --- a/frontend/src/api/modules/container.ts +++ b/frontend/src/api/modules/container.ts @@ -120,8 +120,13 @@ export const DownloadFile = (params: Container.ContainerLogInfo) => { }; // image -export const searchImage = (params: Container.ImageSearch) => { - return http.post>(`/containers/image/search`, params); +export const searchImage = (params: Container.ImageSearch, currentNode?: string) => { + return http.post>( + `/containers/image/search`, + params, + TimeoutEnum.T_60S, + currentNode ? { CurrentNode: currentNode } : undefined, + ); }; export const listAllImage = () => { return http.get>(`/containers/image/all`); diff --git a/frontend/src/components/backup/index.vue b/frontend/src/components/backup/index.vue index 4a4b7d3fe..07be36171 100644 --- a/frontend/src/components/backup/index.vue +++ b/frontend/src/components/backup/index.vue @@ -174,7 +174,6 @@ -