feat: 增加同步本地应用功能 (#537)

This commit is contained in:
zhengkunwang223 2023-04-08 14:02:14 +08:00 committed by GitHub
parent fb62ac17e5
commit 18029d8369
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 247 additions and 42 deletions

View File

@ -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

View File

@ -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"`

View File

@ -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)
}
}

View File

@ -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{})

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -16,6 +16,9 @@ const (
AppOpenresty = "openresty"
AppMysql = "mysql"
AppRedis = "redis"
AppResourceLocal = "local"
AppResourceRemote = "remote"
)
type AppOperate string

View File

@ -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")
)

View File

@ -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 {

View File

@ -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
}

View File

@ -22,6 +22,7 @@ func Init() {
migrations.AddTableSnap,
migrations.AddDefaultGroup,
migrations.AddTableRuntime,
migrations.UpdateTableApp,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View File

@ -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
},
}

View File

@ -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']

View File

@ -75,8 +75,10 @@
</span>
</div>
<div class="app-tag">
<el-tag v-for="(tag, ind) in app.tags" :key="ind" :colr="getColor(ind)">
{{ language == 'zh' ? tag.name : tag.key }}
<el-tag v-for="(tag, ind) in app.tags" :key="ind" style="margin-right: 5px">
<span :style="{ color: getColor(ind) }">
{{ language == 'zh' ? tag.name : tag.key }}
</span>
</el-tag>
</div>
<div class="divider"></div>
@ -113,7 +115,7 @@ let req = reactive({
let apps = ref<App.App[]>([]);
let tags = ref<App.Tag[]>([]);
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);

View File

@ -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<any>();
let loading = ref(false);
let syncLoading = ref(false);
const data = ref<any>();
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<App.Tag[]>([]);
let activeTag = ref('all');
let searchReq = reactive({
const tags = ref<App.Tag[]>([]);
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;

View File

@ -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;