feat: 增加同步本地应用接口 (#6217)

This commit is contained in:
zhengkunwang 2024-08-23 17:22:05 +08:00 committed by GitHub
parent 5865a958fc
commit 034645f50c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 266 additions and 222 deletions

View file

@ -72,7 +72,11 @@ func (b *BaseApi) SyncApp(c *gin.Context) {
// @Router /apps/sync/local [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"应用商店同步","formatEN":"App store synchronization"}
func (b *BaseApi) SyncLocalApp(c *gin.Context) {
go appService.SyncAppListFromLocal()
var req dto.OperateWithTask
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
go appService.SyncAppListFromLocal(req.TaskID)
helper.SuccessWithOutData(c)
}

View file

@ -43,7 +43,7 @@ type IAppService interface {
SyncAppListFromRemote(taskID string) error
GetAppUpdate() (*response.AppUpdateRes, error)
GetAppDetailByID(id uint) (*response.AppDetailDTO, error)
SyncAppListFromLocal()
SyncAppListFromLocal(taskID string)
GetIgnoredApp() ([]response.IgnoredApp, error)
}
@ -489,231 +489,236 @@ func (a AppService) Install(req request.AppInstallCreate) (appInstall *model.App
return
}
func (a AppService) SyncAppListFromLocal() {
fileOp := files.NewFileOp()
localAppDir := constant.LocalAppResourceDir
if !fileOp.Stat(localAppDir) {
return
}
func (a AppService) SyncAppListFromLocal(TaskID string) {
var (
err error
dirEntries []os.DirEntry
localApps []model.App
)
defer func() {
if err != nil {
global.LOG.Errorf("Sync local app failed %v", err)
}
}()
global.LOG.Infof("Starting local application synchronization ...")
dirEntries, err = os.ReadDir(localAppDir)
syncTask, err := task.NewTaskWithOps(i18n.GetMsgByKey("LocalApp"), task.TaskSync, task.TaskScopeAppStore, TaskID, 0)
if err != nil {
global.LOG.Errorf("Create sync task failed %v", err)
return
}
for _, dirEntry := range dirEntries {
if dirEntry.IsDir() {
appDir := filepath.Join(localAppDir, dirEntry.Name())
appDirEntries, err := os.ReadDir(appDir)
if err != nil {
global.LOG.Errorf(i18n.GetMsgWithMap("ErrAppDirNull", map[string]interface{}{"name": dirEntry.Name(), "err": err.Error()}))
continue
}
app, err := handleLocalApp(appDir)
if err != nil {
global.LOG.Errorf(i18n.GetMsgWithMap("LocalAppErr", map[string]interface{}{"name": dirEntry.Name(), "err": err.Error()}))
continue
}
var appDetails []model.AppDetail
for _, appDirEntry := range appDirEntries {
if appDirEntry.IsDir() {
appDetail := model.AppDetail{
Version: appDirEntry.Name(),
Status: constant.AppNormal,
}
versionDir := filepath.Join(appDir, appDirEntry.Name())
if err = handleLocalAppDetail(versionDir, &appDetail); err != nil {
global.LOG.Errorf(i18n.GetMsgWithMap("LocalAppVersionErr", map[string]interface{}{"name": app.Name, "version": appDetail.Version, "err": err.Error()}))
continue
}
appDetails = append(appDetails, appDetail)
}
}
if len(appDetails) > 0 {
app.Details = appDetails
localApps = append(localApps, *app)
} else {
global.LOG.Errorf(i18n.GetMsgWithMap("LocalAppVersionNull", map[string]interface{}{"name": app.Name}))
}
syncTask.AddSubTask(task.GetTaskName(i18n.GetMsgByKey("LocalApp"), task.TaskSync, task.TaskScopeAppStore), func(t *task.Task) (err error) {
fileOp := files.NewFileOp()
localAppDir := constant.LocalAppResourceDir
if !fileOp.Stat(localAppDir) {
return nil
}
}
var (
newApps []model.App
deleteApps []model.App
updateApps []model.App
oldAppIds []uint
deleteAppIds []uint
deleteAppDetails []model.AppDetail
newAppDetails []model.AppDetail
updateDetails []model.AppDetail
appTags []*model.AppTag
)
oldApps, _ := appRepo.GetBy(appRepo.WithResource(constant.AppResourceLocal))
apps := make(map[string]model.App, len(oldApps))
for _, old := range oldApps {
old.Status = constant.AppTakeDown
apps[old.Key] = old
}
for _, app := range localApps {
if oldApp, ok := apps[app.Key]; ok {
app.ID = oldApp.ID
appDetails := make(map[string]model.AppDetail, len(oldApp.Details))
for _, old := range oldApp.Details {
old.Status = constant.AppTakeDown
appDetails[old.Version] = old
}
for i, newDetail := range app.Details {
version := newDetail.Version
newDetail.Status = constant.AppNormal
newDetail.AppId = app.ID
oldDetail, exist := appDetails[version]
if exist {
newDetail.ID = oldDetail.ID
delete(appDetails, version)
}
app.Details[i] = newDetail
}
for _, v := range appDetails {
app.Details = append(app.Details, v)
}
dirEntries, err = os.ReadDir(localAppDir)
if err != nil {
return
}
app.TagsKey = append(app.TagsKey, constant.AppResourceLocal)
apps[app.Key] = app
}
for _, app := range apps {
if app.ID == 0 {
newApps = append(newApps, app)
} else {
oldAppIds = append(oldAppIds, app.ID)
if app.Status == constant.AppTakeDown {
installs, _ := appInstallRepo.ListBy(appInstallRepo.WithAppId(app.ID))
if len(installs) > 0 {
updateApps = append(updateApps, app)
for _, dirEntry := range dirEntries {
if dirEntry.IsDir() {
appDir := filepath.Join(localAppDir, dirEntry.Name())
appDirEntries, err := os.ReadDir(appDir)
if err != nil {
t.Log(i18n.GetWithNameAndErr("ErrAppDirNull", dirEntry.Name(), err))
continue
}
deleteAppIds = append(deleteAppIds, app.ID)
deleteApps = append(deleteApps, app)
deleteAppDetails = append(deleteAppDetails, app.Details...)
} else {
updateApps = append(updateApps, app)
}
}
}
tags, _ := tagRepo.All()
tagMap := make(map[string]uint, len(tags))
for _, tag := range tags {
tagMap[tag.Key] = tag.ID
}
tx, ctx := getTxAndContext()
defer tx.Rollback()
if len(newApps) > 0 {
if err = appRepo.BatchCreate(ctx, newApps); err != nil {
return
}
}
for _, update := range updateApps {
if err = appRepo.Save(ctx, &update); err != nil {
return
}
}
if len(deleteApps) > 0 {
if err = appRepo.BatchDelete(ctx, deleteApps); err != nil {
return
}
if err = appDetailRepo.DeleteByAppIds(ctx, deleteAppIds); err != nil {
return
}
}
if err = appTagRepo.DeleteByAppIds(ctx, oldAppIds); err != nil {
return
}
for _, newApp := range newApps {
if newApp.ID > 0 {
for _, detail := range newApp.Details {
detail.AppId = newApp.ID
newAppDetails = append(newAppDetails, detail)
}
}
}
for _, update := range updateApps {
for _, detail := range update.Details {
if detail.ID == 0 {
detail.AppId = update.ID
newAppDetails = append(newAppDetails, detail)
} else {
if detail.Status == constant.AppNormal {
updateDetails = append(updateDetails, detail)
app, err := handleLocalApp(appDir)
if err != nil {
t.Log(i18n.GetWithNameAndErr("LocalAppErr", dirEntry.Name(), err))
continue
}
var appDetails []model.AppDetail
for _, appDirEntry := range appDirEntries {
if appDirEntry.IsDir() {
appDetail := model.AppDetail{
Version: appDirEntry.Name(),
Status: constant.AppNormal,
}
versionDir := filepath.Join(appDir, appDirEntry.Name())
if err = handleLocalAppDetail(versionDir, &appDetail); err != nil {
t.Log(i18n.GetMsgWithMap("LocalAppVersionErr", map[string]interface{}{"name": app.Name, "version": appDetail.Version, "err": err.Error()}))
continue
}
appDetails = append(appDetails, appDetail)
}
}
if len(appDetails) > 0 {
app.Details = appDetails
localApps = append(localApps, *app)
} else {
deleteAppDetails = append(deleteAppDetails, detail)
t.Log(i18n.GetWithName("LocalAppVersionNull", app.Name))
}
}
}
}
allApps := append(newApps, updateApps...)
for _, app := range allApps {
for _, t := range app.TagsKey {
tagId, ok := tagMap[t]
if ok {
appTags = append(appTags, &model.AppTag{
AppId: app.ID,
TagId: tagId,
})
var (
newApps []model.App
deleteApps []model.App
updateApps []model.App
oldAppIds []uint
deleteAppIds []uint
deleteAppDetails []model.AppDetail
newAppDetails []model.AppDetail
updateDetails []model.AppDetail
appTags []*model.AppTag
)
oldApps, _ := appRepo.GetBy(appRepo.WithResource(constant.AppResourceLocal))
apps := make(map[string]model.App, len(oldApps))
for _, old := range oldApps {
old.Status = constant.AppTakeDown
apps[old.Key] = old
}
for _, app := range localApps {
if oldApp, ok := apps[app.Key]; ok {
app.ID = oldApp.ID
appDetails := make(map[string]model.AppDetail, len(oldApp.Details))
for _, old := range oldApp.Details {
old.Status = constant.AppTakeDown
appDetails[old.Version] = old
}
for i, newDetail := range app.Details {
version := newDetail.Version
newDetail.Status = constant.AppNormal
newDetail.AppId = app.ID
oldDetail, exist := appDetails[version]
if exist {
newDetail.ID = oldDetail.ID
delete(appDetails, version)
}
app.Details[i] = newDetail
}
for _, v := range appDetails {
app.Details = append(app.Details, v)
}
}
app.TagsKey = append(app.TagsKey, constant.AppResourceLocal)
apps[app.Key] = app
}
for _, app := range apps {
if app.ID == 0 {
newApps = append(newApps, app)
} else {
oldAppIds = append(oldAppIds, app.ID)
if app.Status == constant.AppTakeDown {
installs, _ := appInstallRepo.ListBy(appInstallRepo.WithAppId(app.ID))
if len(installs) > 0 {
updateApps = append(updateApps, app)
continue
}
deleteAppIds = append(deleteAppIds, app.ID)
deleteApps = append(deleteApps, app)
deleteAppDetails = append(deleteAppDetails, app.Details...)
} else {
updateApps = append(updateApps, app)
}
}
}
tags, _ := tagRepo.All()
tagMap := make(map[string]uint, len(tags))
for _, tag := range tags {
tagMap[tag.Key] = tag.ID
}
tx, ctx := getTxAndContext()
defer tx.Rollback()
if len(newApps) > 0 {
if err = appRepo.BatchCreate(ctx, newApps); err != nil {
return
}
}
}
if len(newAppDetails) > 0 {
if err = appDetailRepo.BatchCreate(ctx, newAppDetails); err != nil {
return
for _, update := range updateApps {
if err = appRepo.Save(ctx, &update); err != nil {
return
}
}
}
for _, updateAppDetail := range updateDetails {
if err = appDetailRepo.Update(ctx, updateAppDetail); err != nil {
return
if len(deleteApps) > 0 {
if err = appRepo.BatchDelete(ctx, deleteApps); err != nil {
return
}
if err = appDetailRepo.DeleteByAppIds(ctx, deleteAppIds); err != nil {
return
}
}
}
if len(deleteAppDetails) > 0 {
if err = appDetailRepo.BatchDelete(ctx, deleteAppDetails); err != nil {
return
}
}
if len(oldAppIds) > 0 {
if err = appTagRepo.DeleteByAppIds(ctx, oldAppIds); err != nil {
return
}
}
if len(appTags) > 0 {
if err = appTagRepo.BatchCreate(ctx, appTags); err != nil {
return
for _, newApp := range newApps {
if newApp.ID > 0 {
for _, detail := range newApp.Details {
detail.AppId = newApp.ID
newAppDetails = append(newAppDetails, detail)
}
}
}
}
tx.Commit()
global.LOG.Infof("Synchronization of local applications completed")
for _, update := range updateApps {
for _, detail := range update.Details {
if detail.ID == 0 {
detail.AppId = update.ID
newAppDetails = append(newAppDetails, detail)
} else {
if detail.Status == constant.AppNormal {
updateDetails = append(updateDetails, detail)
} else {
deleteAppDetails = append(deleteAppDetails, detail)
}
}
}
}
allApps := append(newApps, updateApps...)
for _, app := range allApps {
for _, t := range app.TagsKey {
tagId, ok := tagMap[t]
if ok {
appTags = append(appTags, &model.AppTag{
AppId: app.ID,
TagId: tagId,
})
}
}
}
if len(newAppDetails) > 0 {
if err = appDetailRepo.BatchCreate(ctx, newAppDetails); err != nil {
return
}
}
for _, updateAppDetail := range updateDetails {
if err = appDetailRepo.Update(ctx, updateAppDetail); err != nil {
return
}
}
if len(deleteAppDetails) > 0 {
if err = appDetailRepo.BatchDelete(ctx, deleteAppDetails); err != nil {
return
}
}
if len(oldAppIds) > 0 {
if err = appTagRepo.DeleteByAppIds(ctx, oldAppIds); err != nil {
return
}
}
if len(appTags) > 0 {
if err = appTagRepo.BatchCreate(ctx, appTags); err != nil {
return
}
}
tx.Commit()
global.LOG.Infof("Synchronization of local applications completed")
return nil
}, nil)
go func() {
_ = syncTask.Execute()
}()
}
func (a AppService) GetAppUpdate() (*response.AppUpdateRes, error) {
@ -941,24 +946,31 @@ func (a AppService) SyncAppListFromRemote(taskID string) (err error) {
}
}
tx, ctx := getTxAndContext()
defer func() {
if err != nil {
tx.Rollback()
return
}
}()
if len(addAppArray) > 0 {
if err = appRepo.BatchCreate(context.Background(), addAppArray); err != nil {
if err = appRepo.BatchCreate(ctx, addAppArray); err != nil {
return
}
}
if len(deleteAppArray) > 0 {
if err = appRepo.BatchDelete(context.Background(), deleteAppArray); err != nil {
if err = appRepo.BatchDelete(ctx, deleteAppArray); err != nil {
return
}
if err = appDetailRepo.DeleteByAppIds(context.Background(), deleteIds); err != nil {
if err = appDetailRepo.DeleteByAppIds(ctx, deleteIds); err != nil {
return
}
}
if err = tagRepo.DeleteAll(context.Background()); err != nil {
if err = tagRepo.DeleteAll(ctx); err != nil {
return
}
if len(tags) > 0 {
if err = tagRepo.BatchCreate(context.Background(), tags); err != nil {
if err = tagRepo.BatchCreate(ctx, tags); err != nil {
return
}
for _, tag := range tags {
@ -966,7 +978,7 @@ func (a AppService) SyncAppListFromRemote(taskID string) (err error) {
}
}
for _, update := range updateAppArray {
if err = appRepo.Save(context.Background(), &update); err != nil {
if err = appRepo.Save(ctx, &update); err != nil {
return
}
}
@ -1011,32 +1023,33 @@ func (a AppService) SyncAppListFromRemote(taskID string) (err error) {
}
}
if len(addDetails) > 0 {
if err = appDetailRepo.BatchCreate(context.Background(), addDetails); err != nil {
if err = appDetailRepo.BatchCreate(ctx, addDetails); err != nil {
return
}
}
if len(deleteDetails) > 0 {
if err = appDetailRepo.BatchDelete(context.Background(), deleteDetails); err != nil {
if err = appDetailRepo.BatchDelete(ctx, deleteDetails); err != nil {
return
}
}
for _, u := range updateDetails {
if err = appDetailRepo.Update(context.Background(), u); err != nil {
if err = appDetailRepo.Update(ctx, u); err != nil {
return
}
}
if len(oldAppIds) > 0 {
if err = appTagRepo.DeleteByAppIds(context.Background(), oldAppIds); err != nil {
if err = appTagRepo.DeleteByAppIds(ctx, oldAppIds); err != nil {
return
}
}
if len(appTags) > 0 {
if err = appTagRepo.BatchCreate(context.Background(), appTags); err != nil {
if err = appTagRepo.BatchCreate(ctx, appTags); err != nil {
return
}
}
tx.Commit()
_ = settingService.Update("AppStoreSyncStatus", constant.SyncSuccess)
_ = settingService.Update("AppStoreLastModified", strconv.Itoa(list.LastModified))

View file

@ -235,4 +235,5 @@ AppLink: 'Associated Application'
EnableSSL: "Enable HTTPS"
AppStore: "App Store"
TaskSync: "Sync"
LocalApp: "Local App"

View file

@ -237,4 +237,5 @@ AppLink: '關聯應用'
EnableSSL: "開啟 HTTPS"
AppStore: "應用商店"
TaskSync: "同步"
LocalApp: "本地應用"

View file

@ -238,3 +238,4 @@ AppLink: "关联应用"
EnableSSL: "开启 HTTPS"
AppStore: "应用商店"
TaskSync: "同步"
LocalApp: "本地应用"

View file

@ -13,7 +13,8 @@ func (a *AppRouter) InitRouter(Router *gin.RouterGroup) {
baseApi := v2.ApiGroupApp.BaseApi
{
appRouter.POST("/sync", baseApi.SyncApp)
appRouter.POST("/sync/remote", baseApi.SyncApp)
appRouter.POST("/sync/local", baseApi.SyncLocalApp)
appRouter.GET("/checkupdate", baseApi.GetAppListUpdate)
appRouter.POST("/search", baseApi.SearchApp)
appRouter.GET("/:key", baseApi.GetApp)

View file

@ -4,7 +4,11 @@ import { App } from '../interface/app';
import { TimeoutEnum } from '@/enums/http-enum';
export const SyncApp = (req: App.AppStoreSync) => {
return http.post('apps/sync', req);
return http.post('apps/sync/remote', req);
};
export const SyncLocalApp = (req: App.AppStoreSync) => {
return http.post('apps/sync/local', req);
};
export const GetAppListUpdate = () => {

View file

@ -1878,6 +1878,7 @@ const message = {
supportedArchitectures: 'Architectures',
link: 'Link',
showCurrentArch: 'Architecture',
syncLocalApp: 'Sync Local App',
},
website: {
website: 'Website',

View file

@ -1744,6 +1744,7 @@ const message = {
supportedArchitectures: '支持架構',
link: '鏈接',
showCurrentArch: '僅顯示當前架構',
syncLocalApp: '同步本地應用',
},
website: {
website: '網站',

View file

@ -1686,7 +1686,7 @@ const message = {
noService: '{0}',
toInstall: '去安装',
param: '参数配置',
syncAppList: '更新应用列表',
syncAppList: '更新远程应用',
alreadyRun: '已安装',
less1Minute: '小于1分钟',
appOfficeWebsite: '官方网站',
@ -1745,6 +1745,7 @@ const message = {
supportedArchitectures: '支持架构',
link: '链接',
showCurrentArch: '仅显示当前服务器架构应用',
syncLocalApp: '同步本地应用',
},
website: {
website: '网站',

View file

@ -53,11 +53,12 @@
</el-row>
</template>
<template #leftToolBar>
<el-badge is-dot :hidden="!canUpdate">
<el-button @click="sync" type="primary" plain :disabled="syncing">
{{ $t('app.syncAppList') }}
</el-button>
</el-badge>
<el-button @click="sync" type="primary" plain :disabled="syncing">
{{ $t('app.syncAppList') }}
</el-button>
<el-button @click="syncLocal" type="primary" plain :disabled="syncing" class="ml-2">
{{ $t('app.syncLocalApp') }}
</el-button>
</template>
<template #rightToolBar>
<el-checkbox class="!mr-2.5" v-model="req.showCurrentArch" @change="search(req)">
@ -174,8 +175,7 @@
<script lang="ts" setup>
import { App } from '@/api/interface/app';
import { onMounted, reactive, ref, computed } from 'vue';
import { GetAppTags, SearchApp, SyncApp } from '@/api/modules/app';
import i18n from '@/lang';
import { GetAppTags, SearchApp, SyncApp, SyncLocalApp } from '@/api/modules/app';
import Install from '../detail/install/index.vue';
import router from '@/routers';
import { MsgSuccess } from '@/utils/message';
@ -281,7 +281,6 @@ const sync = () => {
if (res.message != '') {
MsgSuccess(res.message);
} else {
MsgSuccess(i18n.global.t('app.syncStart'));
openTaskLog(taskID);
}
canUpdate.value = false;
@ -292,6 +291,23 @@ const sync = () => {
});
};
const syncLocal = () => {
const taskID = newUUID();
const syncReq = {
taskID: taskID,
};
syncing.value = true;
SyncLocalApp(syncReq)
.then(() => {
openTaskLog(taskID);
canUpdate.value = false;
search(req);
})
.finally(() => {
syncing.value = false;
});
};
const changeTag = (key: string) => {
req.tags = [];
activeTag.value = key;