package data import ( "encoding/json" "errors" "fmt" "log/slog" "slices" "github.com/expr-lang/expr" "github.com/hashicorp/go-version" "github.com/knadh/koanf/v2" "github.com/leonelquinteros/gotext" "github.com/spf13/cast" "gorm.io/gorm" "github.com/acepanel/panel/internal/app" "github.com/acepanel/panel/internal/biz" "github.com/acepanel/panel/pkg/api" "github.com/acepanel/panel/pkg/shell" ) type appRepo struct { t *gotext.Locale conf *koanf.Koanf db *gorm.DB log *slog.Logger cache biz.CacheRepo task biz.TaskRepo api *api.API } func NewAppRepo(t *gotext.Locale, conf *koanf.Koanf, db *gorm.DB, log *slog.Logger, cache biz.CacheRepo, task biz.TaskRepo) biz.AppRepo { return &appRepo{ t: t, conf: conf, db: db, log: log, cache: cache, task: task, api: api.NewAPI(app.Version, app.Locale), } } func (r *appRepo) All() api.Apps { cached, err := r.cache.Get(biz.CacheKeyApps) if err != nil { return nil } var apps api.Apps if err = json.Unmarshal([]byte(cached), &apps); err != nil { return nil } return apps } func (r *appRepo) Get(slug string) (*api.App, error) { for item := range slices.Values(r.All()) { if item.Slug == slug { return item, nil } } return nil, errors.New(r.t.Get("app %s not found", slug)) } func (r *appRepo) UpdateExist(slug string) bool { item, err := r.Get(slug) if err != nil { return false } installed, err := r.GetInstalled(slug) if err != nil { return false } for channel := range slices.Values(item.Channels) { if channel.Slug == installed.Channel { if channel.Version != installed.Version { return true } } } return false } func (r *appRepo) Installed() ([]*biz.App, error) { var apps []*biz.App if err := r.db.Find(&apps).Error; err != nil { return nil, err } return apps, nil } func (r *appRepo) GetInstalled(slug string) (*biz.App, error) { installed := new(biz.App) if err := r.db.Where("slug = ?", slug).First(installed).Error; err != nil { return nil, err } return installed, nil } func (r *appRepo) GetInstalledAll(query string, cond ...string) ([]*biz.App, error) { var apps []*biz.App if err := r.db.Where(query, cond).Find(&apps).Error; err != nil { return nil, err } return apps, nil } func (r *appRepo) GetHomeShow() ([]map[string]string, error) { var apps []*biz.App if err := r.db.Where("show = ?", true).Order("show_order").Find(&apps).Error; err != nil { return nil, err } filtered := make([]map[string]string, 0) for item := range slices.Values(apps) { loaded, err := r.Get(item.Slug) if err != nil { continue } filtered = append(filtered, map[string]string{ "name": loaded.Name, "description": loaded.Description, "slug": loaded.Slug, "icon": loaded.Icon, "version": item.Version, }) } return filtered, nil } func (r *appRepo) IsInstalled(query string, cond ...string) (bool, error) { var count int64 if len(cond) == 0 { if err := r.db.Model(&biz.App{}).Where("slug = ?", query).Count(&count).Error; err != nil { return false, err } } else { if err := r.db.Model(&biz.App{}).Where(query, cond).Count(&count).Error; err != nil { return false, err } } return count > 0, nil } func (r *appRepo) Install(channel, slug string) error { item, err := r.Get(slug) if err != nil { return err } panel, err := version.NewVersion(app.Version) if err != nil { return err } if installed, _ := r.IsInstalled(slug); installed { return errors.New(r.t.Get("app %s already installed", slug)) } shellUrl, shellChannel, shellVersion := "", "", "" for ch := range slices.Values(item.Channels) { vs, err := version.NewVersion(ch.Panel) if err != nil { continue } if ch.Slug == channel { if vs.GreaterThan(panel) && !r.conf.Bool("app.debug") { return errors.New(r.t.Get("app %s requires panel version %s, current version %s", item.Name, ch.Panel, app.Version)) } shellUrl = ch.Install shellChannel = ch.Slug shellVersion = ch.Version break } } if shellUrl == "" { return errors.New(r.t.Get("app %s not support current panel version", item.Name)) } if err = r.preCheck(item); err != nil { return err } // 下载回调 if err = r.api.AppCallback(slug); err != nil { r.log.Warn("[App] download callback failed", slog.String("app", slug), slog.Any("error", err)) } if app.IsCli { return shell.ExecfWithOutput(`curl -sSLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion) } task := new(biz.Task) task.Name = r.t.Get("Install app %s", item.Name) task.Status = biz.TaskStatusWaiting task.Shell = fmt.Sprintf(`curl -sSLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug) task.Log = "/tmp/" + item.Slug + ".log" return r.task.Push(task) } func (r *appRepo) UnInstall(slug string) error { item, err := r.Get(slug) if err != nil { return err } panel, err := version.NewVersion(app.Version) if err != nil { return err } if installed, _ := r.IsInstalled(slug); !installed { return errors.New(r.t.Get("app %s not installed", item.Name)) } installed, err := r.GetInstalled(slug) if err != nil { return err } shellUrl, shellChannel, shellVersion := "", "", "" for ch := range slices.Values(item.Channels) { vs, err := version.NewVersion(ch.Panel) if err != nil { continue } if ch.Slug == installed.Channel { if vs.GreaterThan(panel) && !r.conf.Bool("app.debug") { return errors.New(r.t.Get("app %s requires panel version %s, current version %s", item.Name, ch.Panel, app.Version)) } shellUrl = ch.Uninstall shellChannel = ch.Slug shellVersion = installed.Version break } } if shellUrl == "" { return errors.New(r.t.Get("failed to get uninstall script for app %s", item.Name)) } if err = r.preCheck(item); err != nil { return err } if app.IsCli { return shell.ExecfWithOutput(`curl -sSLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion) } task := new(biz.Task) task.Name = r.t.Get("Uninstall app %s", item.Name) task.Status = biz.TaskStatusWaiting task.Shell = fmt.Sprintf(`curl -sSLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug) task.Log = "/tmp/" + item.Slug + ".log" return r.task.Push(task) } func (r *appRepo) Update(slug string) error { item, err := r.Get(slug) if err != nil { return err } panel, err := version.NewVersion(app.Version) if err != nil { return err } if installed, _ := r.IsInstalled(slug); !installed { return errors.New(r.t.Get("app %s not installed", item.Name)) } installed, err := r.GetInstalled(slug) if err != nil { return err } shellUrl, shellChannel, shellVersion := "", "", "" for ch := range slices.Values(item.Channels) { vs, err := version.NewVersion(ch.Panel) if err != nil { continue } if ch.Slug == installed.Channel { if vs.GreaterThan(panel) && !r.conf.Bool("app.debug") { return errors.New(r.t.Get("app %s requires panel version %s, current version %s", item.Name, ch.Panel, app.Version)) } shellUrl = ch.Update shellChannel = ch.Slug shellVersion = ch.Version break } } if shellUrl == "" { return errors.New(r.t.Get("app %s not support current panel version", item.Name)) } if err = r.preCheck(item); err != nil { return err } // 下载回调 if err = r.api.AppCallback(slug); err != nil { r.log.Warn("[App] download callback failed", slog.String("app", slug), slog.Any("error", err)) } if app.IsCli { return shell.ExecfWithOutput(`curl -sSLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion) } task := new(biz.Task) task.Name = r.t.Get("Update app %s", item.Name) task.Status = biz.TaskStatusWaiting task.Shell = fmt.Sprintf(`curl -sSLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug) task.Log = "/tmp/" + item.Slug + ".log" return r.task.Push(task) } func (r *appRepo) UpdateShow(slug string, show bool) error { item, err := r.GetInstalled(slug) if err != nil { return err } item.Show = show return r.db.Save(item).Error } func (r *appRepo) preCheck(app *api.App) error { var apps []string var installed []string all := r.All() for _, item := range all { apps = append(apps, item.Slug) } installedApps, err := r.Installed() if err != nil { return err } for _, item := range installedApps { installed = append(installed, item.Slug) } env := map[string]any{ "apps": apps, "installed": installed, } output, err := expr.Eval(app.Depends, env) if err != nil { return err } result := cast.ToString(output) if result != "ok" { return errors.New(r.t.Get("App %s %s", app.Name, result)) } return nil }