From 18029d8369bfd3f2e3ba8d2698ec57b3be7c3b25 Mon Sep 17 00:00:00 2001 From: zhengkunwang223 <31820853+zhengkunwang223@users.noreply.github.com> Date: Sat, 8 Apr 2023 14:02:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=BA=94=E7=94=A8=E5=8A=9F=E8=83=BD=20(#537)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/app.go | 3 +- backend/app/model/app.go | 1 + backend/app/model/app_install.go | 15 +- backend/app/repo/app.go | 7 + backend/app/service/app.go | 163 +++++++++++++++++- backend/app/service/app_utils.go | 31 +++- backend/app/service/backup_app.go | 3 +- backend/constant/app.go | 3 + backend/constant/dir.go | 12 +- backend/init/app/app.go | 4 +- backend/init/business/business.go | 2 +- backend/init/migration/migrate.go | 1 + backend/init/migration/migrations/init.go | 10 ++ frontend/components.d.ts | 1 + frontend/src/views/app-store/apps/index.vue | 8 +- .../src/views/app-store/installed/index.vue | 24 +-- .../src/views/host/file-management/index.vue | 1 + 17 files changed, 247 insertions(+), 42 deletions(-) diff --git a/backend/app/api/v1/app.go b/backend/app/api/v1/app.go index bbd2696ed..1ed5b009a 100644 --- a/backend/app/api/v1/app.go +++ b/backend/app/api/v1/app.go @@ -38,8 +38,9 @@ func (b *BaseApi) SearchApp(c *gin.Context) { // @Router /apps/sync [post] // @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFuntions":[],"formatZH":"应用商店同步","formatEN":"App store synchronization"} func (b *BaseApi) SyncApp(c *gin.Context) { + appService.SyncAppListFromLocal() global.LOG.Infof("sync app list start ...") - if err := appService.SyncAppList(); err != nil { + if err := appService.SyncAppListFromRemote(); err != nil { global.LOG.Errorf("sync app list error [%s]", err.Error()) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) return diff --git a/backend/app/model/app.go b/backend/app/model/app.go index 58b4b834f..1772d4042 100644 --- a/backend/app/model/app.go +++ b/backend/app/model/app.go @@ -16,6 +16,7 @@ type App struct { Github string `json:"github" gorm:"type:varchar(64);not null"` Document string `json:"document" gorm:"type:varchar(64);not null"` Recommend int `json:"recommend" gorm:"type:Integer;not null"` + Resource string `json:"resource" gorm:"type:varchar;not null;default:remote"` Details []AppDetail `json:"-" gorm:"-:migration"` TagsKey []string `json:"-" gorm:"-"` AppTags []AppTag `json:"-" gorm:"-:migration"` diff --git a/backend/app/model/app_install.go b/backend/app/model/app_install.go index b66d9f162..0049b637d 100644 --- a/backend/app/model/app_install.go +++ b/backend/app/model/app_install.go @@ -2,6 +2,7 @@ package model import ( "path" + "strings" "github.com/1Panel-dev/1Panel/backend/constant" ) @@ -26,13 +27,21 @@ type AppInstall struct { } func (i *AppInstall) GetPath() string { - return path.Join(constant.AppInstallDir, i.App.Key, i.Name) + return path.Join(i.getAppPath(), i.Name) } func (i *AppInstall) GetComposePath() string { - return path.Join(constant.AppInstallDir, i.App.Key, i.Name, "docker-compose.yml") + return path.Join(i.getAppPath(), i.Name, "docker-compose.yml") } func (i *AppInstall) GetEnvPath() string { - return path.Join(constant.AppInstallDir, i.App.Key, i.Name, ".env") + return path.Join(i.getAppPath(), i.Name, ".env") +} + +func (i *AppInstall) getAppPath() string { + if i.App.Resource == constant.AppResourceLocal { + return path.Join(constant.LocalAppInstallDir, strings.TrimPrefix(i.App.Key, constant.AppResourceLocal)) + } else { + return path.Join(constant.AppInstallDir, i.App.Key) + } } diff --git a/backend/app/repo/app.go b/backend/app/repo/app.go index b3810f8d7..0e9ca1af9 100644 --- a/backend/app/repo/app.go +++ b/backend/app/repo/app.go @@ -16,6 +16,7 @@ type IAppRepo interface { WithType(typeStr string) DBOption OrderByRecommend() DBOption GetRecommend() DBOption + WithResource(resource string) DBOption Page(page, size int, opts ...DBOption) (int64, []model.App, error) GetFirst(opts ...DBOption) (model.App, error) GetBy(opts ...DBOption) ([]model.App, error) @@ -53,6 +54,12 @@ func (a AppRepo) GetRecommend() DBOption { } } +func (a AppRepo) WithResource(resource string) DBOption { + return func(g *gorm.DB) *gorm.DB { + return g.Where("resource = ?", resource) + } +} + func (a AppRepo) Page(page, size int, opts ...DBOption) (int64, []model.App, error) { var apps []model.App db := getDb(opts...).Model(&model.App{}) diff --git a/backend/app/service/app.go b/backend/app/service/app.go index fe8d18163..fadbfe610 100644 --- a/backend/app/service/app.go +++ b/backend/app/service/app.go @@ -35,9 +35,10 @@ type IAppService interface { GetApp(key string) (*response.AppDTO, error) GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error) Install(ctx context.Context, req request.AppInstallCreate) (*model.AppInstall, error) - SyncAppList() error + SyncAppListFromRemote() error GetAppUpdate() (*response.AppUpdateRes, error) GetAppDetailByID(id uint) (*response.AppDetailDTO, error) + SyncAppListFromLocal() } func NewIAppService() IAppService { @@ -294,7 +295,7 @@ func (a AppService) Install(ctx context.Context, req request.AppInstallCreate) ( } appInstall.DockerCompose = string(composeByte) - if err := copyAppData(app.Key, appDetail.Version, req.Name, req.Params); err != nil { + if err := copyAppData(app.Key, appDetail.Version, req.Name, req.Params, app.Resource == constant.AppResourceLocal); err != nil { return nil, err } fileOp := files.NewFileOp() @@ -360,7 +361,156 @@ func (a AppService) GetAppUpdate() (*response.AppUpdateRes, error) { return res, nil } -func (a AppService) SyncAppList() error { +func (a AppService) SyncAppListFromLocal() { + fileOp := files.NewFileOp() + appDir := constant.LocalAppResourceDir + listFile := path.Join(appDir, "list.json") + if !fileOp.Stat(listFile) { + return + } + global.LOG.Infof("start sync local apps...") + content, err := fileOp.GetContent(listFile) + if err != nil { + global.LOG.Errorf("get list.json content failed %s", err.Error()) + return + } + list := &dto.AppList{} + if err := json.Unmarshal(content, list); err != nil { + global.LOG.Errorf("unmarshal list.json failed %s", err.Error()) + return + } + oldApps, _ := appRepo.GetBy(appRepo.WithResource(constant.AppResourceLocal)) + appsMap := getApps(oldApps, list.Items, true) + for _, l := range list.Items { + localKey := "local" + l.Key + app := appsMap[localKey] + icon, err := os.ReadFile(path.Join(appDir, l.Key, "metadata", "logo.png")) + if err != nil { + global.LOG.Errorf("get [%s] icon error: %s", l.Name, err.Error()) + continue + } + iconStr := base64.StdEncoding.EncodeToString(icon) + app.Icon = iconStr + app.TagsKey = append(l.Tags, "Local") + app.Recommend = 9999 + versions := l.Versions + detailsMap := getAppDetails(app.Details, versions) + + for _, v := range versions { + detail := detailsMap[v] + detailPath := path.Join(appDir, l.Key, "versions", v) + if _, err := os.Stat(detailPath); err != nil { + global.LOG.Errorf("get [%s] folder error: %s", detailPath, err.Error()) + continue + } + readmeStr, err := os.ReadFile(path.Join(detailPath, "README.md")) + if err != nil { + global.LOG.Errorf("get [%s] README error: %s", detailPath, err.Error()) + } + detail.Readme = string(readmeStr) + dockerComposeStr, err := os.ReadFile(path.Join(detailPath, "docker-compose.yml")) + if err != nil { + global.LOG.Errorf("get [%s] docker-compose.yml error: %s", detailPath, err.Error()) + continue + } + detail.DockerCompose = string(dockerComposeStr) + paramStr, err := os.ReadFile(path.Join(detailPath, "config.json")) + if err != nil { + global.LOG.Errorf("get [%s] form.json error: %s", detailPath, err.Error()) + } + detail.Params = string(paramStr) + detailsMap[v] = detail + } + var newDetails []model.AppDetail + for _, v := range detailsMap { + newDetails = append(newDetails, v) + } + app.Details = newDetails + appsMap[localKey] = app + } + var ( + addAppArray []model.App + updateArray []model.App + appIds []uint + ) + for _, v := range appsMap { + if v.ID == 0 { + addAppArray = append(addAppArray, v) + } else { + updateArray = append(updateArray, v) + appIds = append(appIds, v.ID) + } + } + tx, ctx := getTxAndContext() + if len(addAppArray) > 0 { + if err := appRepo.BatchCreate(ctx, addAppArray); err != nil { + tx.Rollback() + return + } + } + for _, update := range updateArray { + if err := appRepo.Save(ctx, &update); err != nil { + tx.Rollback() + return + } + } + if err := appTagRepo.DeleteByAppIds(ctx, appIds); err != nil { + tx.Rollback() + return + } + apps := append(addAppArray, updateArray...) + var ( + addDetails []model.AppDetail + updateDetails []model.AppDetail + appTags []*model.AppTag + ) + tags, _ := tagRepo.All() + tagMap := make(map[string]uint, len(tags)) + for _, app := range tags { + tagMap[app.Key] = app.ID + } + for _, a := range apps { + for _, t := range a.TagsKey { + tagId, ok := tagMap[t] + if ok { + appTags = append(appTags, &model.AppTag{ + AppId: a.ID, + TagId: tagId, + }) + } + } + for _, d := range a.Details { + d.AppId = a.ID + if d.ID == 0 { + addDetails = append(addDetails, d) + } else { + updateDetails = append(updateDetails, d) + } + } + } + if len(addDetails) > 0 { + if err := appDetailRepo.BatchCreate(ctx, addDetails); err != nil { + tx.Rollback() + return + } + } + for _, u := range updateDetails { + if err := appDetailRepo.Update(ctx, u); err != nil { + tx.Rollback() + return + } + } + if len(appTags) > 0 { + if err := appTagRepo.BatchCreate(ctx, appTags); err != nil { + tx.Rollback() + return + } + } + tx.Commit() + global.LOG.Infof("sync local apps success") + return +} +func (a AppService) SyncAppListFromRemote() error { updateRes, err := a.GetAppUpdate() if err != nil { return err @@ -393,11 +543,11 @@ func (a AppService) SyncAppList() error { Name: t.Name, }) } - oldApps, err := appRepo.GetBy() + oldApps, err := appRepo.GetBy(appRepo.WithResource(constant.AppResourceRemote)) if err != nil { return err } - appsMap := getApps(oldApps, list.Items) + appsMap := getApps(oldApps, list.Items, false) for _, l := range list.Items { app := appsMap[l.Key] icon, err := os.ReadFile(path.Join(appDir, l.Key, "metadata", "logo.png")) @@ -453,8 +603,9 @@ func (a AppService) SyncAppList() error { var ( addAppArray []model.App updateArray []model.App + tagMap = make(map[string]uint, len(tags)) ) - tagMap := make(map[string]uint, len(tags)) + for _, v := range appsMap { if v.ID == 0 { addAppArray = append(addAppArray, v) diff --git a/backend/app/service/app_utils.go b/backend/app/service/app_utils.go index 7b12c9f40..f9df2202e 100644 --- a/backend/app/service/app_utils.go +++ b/backend/app/service/app_utils.go @@ -293,11 +293,9 @@ func checkLimit(app model.App) error { } func checkRequiredAndLimit(app model.App) error { - if err := checkLimit(app); err != nil { return err } - if app.Required != "" { var requiredArray []string if err := json.Unmarshal([]byte(app.Required), &requiredArray); err != nil { @@ -326,7 +324,6 @@ func checkRequiredAndLimit(app model.App) error { } } } - return nil } @@ -343,10 +340,17 @@ func handleMap(params map[string]interface{}, envParams map[string]string) { } } -func copyAppData(key, version, installName string, params map[string]interface{}) (err error) { +func copyAppData(key, version, installName string, params map[string]interface{}, isLocal bool) (err error) { fileOp := files.NewFileOp() - resourceDir := path.Join(constant.AppResourceDir, key, "versions", version) + appResourceDir := constant.AppResourceDir installAppDir := path.Join(constant.AppInstallDir, key) + appKey := key + if isLocal { + appResourceDir = constant.LocalAppResourceDir + appKey = strings.TrimPrefix(key, "local") + installAppDir = path.Join(constant.LocalAppInstallDir, appKey) + } + resourceDir := path.Join(appResourceDir, appKey, "versions", version) if !fileOp.Stat(installAppDir) { if err = fileOp.CreateDir(installAppDir, 0755); err != nil { @@ -471,20 +475,29 @@ func getAppDetails(details []model.AppDetail, versions []string) map[string]mode return appDetails } -func getApps(oldApps []model.App, items []dto.AppDefine) map[string]model.App { +func getApps(oldApps []model.App, items []dto.AppDefine, isLocal bool) map[string]model.App { apps := make(map[string]model.App, len(oldApps)) for _, old := range oldApps { old.Status = constant.AppTakeDown apps[old.Key] = old } for _, item := range items { - app, ok := apps[item.Key] + key := item.Key + if isLocal { + key = "local" + key + } + app, ok := apps[key] if !ok { app = model.App{} } + if isLocal { + app.Resource = constant.AppResourceLocal + } else { + app.Resource = constant.AppResourceRemote + } app.Name = item.Name app.Limit = item.Limit - app.Key = item.Key + app.Key = key app.ShortDescZh = item.ShortDescZh app.ShortDescEn = item.ShortDescEn app.Website = item.Website @@ -494,7 +507,7 @@ func getApps(oldApps []model.App, items []dto.AppDefine) map[string]model.App { app.CrossVersionUpdate = item.CrossVersionUpdate app.Required = item.GetRequired() app.Status = constant.AppNormal - apps[item.Key] = app + apps[key] = app } return apps } diff --git a/backend/app/service/backup_app.go b/backend/app/service/backup_app.go index fc2a1cf5b..c8ef87ce9 100644 --- a/backend/app/service/backup_app.go +++ b/backend/app/service/backup_app.go @@ -33,6 +33,7 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) error { return err } timeNow := time.Now().Format("20060102150405") + backupDir := fmt.Sprintf("%s/app/%s/%s", localDir, req.Name, req.DetailName) fileName := fmt.Sprintf("%s_%s.tar.gz", req.DetailName, timeNow) @@ -98,7 +99,7 @@ func handleAppBackup(install *model.AppInstall, backupDir, fileName string) erro return err } - appPath := fmt.Sprintf("%s/%s/%s", constant.AppInstallDir, install.App.Key, install.Name) + appPath := fmt.Sprintf("%s/%s", install.GetPath(), install.Name) if err := handleTar(appPath, tmpDir, "app.tar.gz", ""); err != nil { return err } diff --git a/backend/constant/app.go b/backend/constant/app.go index 526a0afb4..9ef2e59d9 100644 --- a/backend/constant/app.go +++ b/backend/constant/app.go @@ -16,6 +16,9 @@ const ( AppOpenresty = "openresty" AppMysql = "mysql" AppRedis = "redis" + + AppResourceLocal = "local" + AppResourceRemote = "remote" ) type AppOperate string diff --git a/backend/constant/dir.go b/backend/constant/dir.go index aa5da639c..89615524c 100644 --- a/backend/constant/dir.go +++ b/backend/constant/dir.go @@ -7,9 +7,11 @@ import ( ) var ( - DataDir = global.CONF.System.DataDir - ResourceDir = path.Join(DataDir, "resource") - AppResourceDir = path.Join(ResourceDir, "apps") - AppInstallDir = path.Join(DataDir, "apps") - RuntimeDir = path.Join(DataDir, "runtime") + DataDir = global.CONF.System.DataDir + ResourceDir = path.Join(DataDir, "resource") + AppResourceDir = path.Join(ResourceDir, "apps") + AppInstallDir = path.Join(DataDir, "apps") + LocalAppResourceDir = path.Join(ResourceDir, "localApps") + LocalAppInstallDir = path.Join(DataDir, "localApps") + RuntimeDir = path.Join(DataDir, "runtime") ) diff --git a/backend/init/app/app.go b/backend/init/app/app.go index 8fea7bcca..a02812521 100644 --- a/backend/init/app/app.go +++ b/backend/init/app/app.go @@ -15,8 +15,10 @@ func Init() { constant.AppResourceDir = path.Join(constant.ResourceDir, "apps") constant.AppInstallDir = path.Join(constant.DataDir, "apps") constant.RuntimeDir = path.Join(constant.DataDir, "runtime") + constant.LocalAppResourceDir = path.Join(constant.ResourceDir, "localApps") + constant.LocalAppInstallDir = path.Join(constant.DataDir, "localApps") - dirs := []string{constant.DataDir, constant.ResourceDir, constant.AppResourceDir, constant.AppInstallDir, global.CONF.System.Backup, constant.RuntimeDir} + dirs := []string{constant.DataDir, constant.ResourceDir, constant.AppResourceDir, constant.AppInstallDir, global.CONF.System.Backup, constant.RuntimeDir, constant.LocalAppResourceDir} fileOp := files.NewFileOp() for _, dir := range dirs { diff --git a/backend/init/business/business.go b/backend/init/business/business.go index 0f33a6f52..a1520b8ff 100644 --- a/backend/init/business/business.go +++ b/backend/init/business/business.go @@ -21,7 +21,7 @@ func syncApp() { return } global.LOG.Info("sync app start...") - if err := service.NewIAppService().SyncAppList(); err != nil { + if err := service.NewIAppService().SyncAppListFromRemote(); err != nil { global.LOG.Errorf("sync app error: %s", err.Error()) return } diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index 29c50c0c5..0b37edaff 100644 --- a/backend/init/migration/migrate.go +++ b/backend/init/migration/migrate.go @@ -22,6 +22,7 @@ func Init() { migrations.AddTableSnap, migrations.AddDefaultGroup, migrations.AddTableRuntime, + migrations.UpdateTableApp, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/backend/init/migration/migrations/init.go b/backend/init/migration/migrations/init.go index e6ef8098e..1b3dc6e1c 100644 --- a/backend/init/migration/migrations/init.go +++ b/backend/init/migration/migrations/init.go @@ -254,3 +254,13 @@ var AddTableRuntime = &gormigrate.Migration{ return tx.AutoMigrate(&model.Runtime{}, &model.Website{}) }, } + +var UpdateTableApp = &gormigrate.Migration{ + ID: "20230408-update-table-app", + Migrate: func(tx *gorm.DB) error { + if err := tx.AutoMigrate(&model.App{}); err != nil { + return err + } + return nil + }, +} diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 254206846..5c341cc4a 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -87,6 +87,7 @@ declare module 'vue' { SvgIcon: typeof import('./src/components/svg-icon/svg-icon.vue')['default'] SystemUpgrade: typeof import('./src/components/system-upgrade/index.vue')['default'] TableSetting: typeof import('./src/components/table-setting/index.vue')['default'] + Terminal: typeof import('./src/components/terminal/index.vue')['default'] Tooltip: typeof import('./src/components/tooltip/index.vue')['default'] Upload: typeof import('./src/components/upload/index.vue')['default'] VCharts: typeof import('./src/components/v-charts/index.vue')['default'] diff --git a/frontend/src/views/app-store/apps/index.vue b/frontend/src/views/app-store/apps/index.vue index 0b7ce3e7d..4e3d90790 100644 --- a/frontend/src/views/app-store/apps/index.vue +++ b/frontend/src/views/app-store/apps/index.vue @@ -75,8 +75,10 @@
- - {{ language == 'zh' ? tag.name : tag.key }} + + + {{ language == 'zh' ? tag.name : tag.key }} +
@@ -113,7 +115,7 @@ let req = reactive({ let apps = ref([]); let tags = ref([]); -const colorArr = ['#6495ED', '#54FF9F', '#BEBEBE', '#FFF68F', '#FFFF00', '#8B0000']; +const colorArr = ['#005eeb', '#008B45', '#BEBEBE', '#FFF68F', '#FFFF00', '#8B0000']; let loading = ref(false); let activeTag = ref('all'); let showDetail = ref(false); diff --git a/frontend/src/views/app-store/installed/index.vue b/frontend/src/views/app-store/installed/index.vue index 10058d099..47a4f7c3a 100644 --- a/frontend/src/views/app-store/installed/index.vue +++ b/frontend/src/views/app-store/installed/index.vue @@ -105,7 +105,7 @@ round size="small" :disabled="installed.status !== 'Running'" - @click="openUploads(installed.app.key, installed.name)" + @click="openUploads(installed.app.name, installed.name)" v-if="mode === 'installed'" > {{ $t('database.loadBackup') }} @@ -117,7 +117,7 @@ round size="small" :disabled="installed.status !== 'Running'" - @click="openBackups(installed.app.key, installed.name)" + @click="openBackups(installed.app.name, installed.name)" v-if="mode === 'installed'" > {{ $t('app.backup') }} @@ -201,17 +201,17 @@ import { getAge } from '@/utils/util'; import { useRouter } from 'vue-router'; import { MsgSuccess } from '@/utils/message'; -let data = ref(); -let loading = ref(false); -let syncLoading = ref(false); +const data = ref(); +const loading = ref(false); +const syncLoading = ref(false); let timer: NodeJS.Timer | null = null; const paginationConfig = reactive({ currentPage: 1, pageSize: 20, total: 0, }); -let open = ref(false); -let operateReq = reactive({ +const open = ref(false); +const operateReq = reactive({ installId: 0, operate: '', detailId: 0, @@ -222,9 +222,9 @@ const checkRef = ref(); const deleteRef = ref(); const appParamRef = ref(); const upgradeRef = ref(); -let tags = ref([]); -let activeTag = ref('all'); -let searchReq = reactive({ +const tags = ref([]); +const activeTag = ref('all'); +const searchReq = reactive({ page: 1, pageSize: 15, name: '', @@ -232,8 +232,8 @@ let searchReq = reactive({ update: false, }); const router = useRouter(); -let activeName = ref(i18n.global.t('app.installed')); -let mode = ref('installed'); +const activeName = ref(i18n.global.t('app.installed')); +const mode = ref('installed'); const sync = () => { syncLoading.value = true; diff --git a/frontend/src/views/host/file-management/index.vue b/frontend/src/views/host/file-management/index.vue index c81ad1783..51b22ce57 100644 --- a/frontend/src/views/host/file-management/index.vue +++ b/frontend/src/views/host/file-management/index.vue @@ -174,6 +174,7 @@ import Process from './process/index.vue'; import { useRouter } from 'vue-router'; import { Back, Refresh } from '@element-plus/icons-vue'; import { MsgSuccess, MsgWarning } from '@/utils/message'; +import { ElMessageBox } from 'element-plus'; interface FilePaths { url: string;