feat: 应用商店支持使用自定的应用包

This commit is contained in:
zhengkunwang223 2024-10-12 17:14:02 +08:00
parent 72311617d9
commit b410004aef
15 changed files with 227 additions and 74 deletions

View file

@ -45,6 +45,7 @@ type AppDto struct {
Github string `json:"github"`
Website string `json:"website"`
GpuSupport bool `json:"gpuSupport"`
Recommend int `json:"recommend"`
}
type TagDTO struct {

View file

@ -30,6 +30,7 @@ type IAppRepo interface {
Create(ctx context.Context, app *model.App) error
Save(ctx context.Context, app *model.App) error
BatchDelete(ctx context.Context, apps []model.App) error
DeleteByIDs(ctx context.Context, ids []uint) error
}
func NewIAppRepo() IAppRepo {
@ -91,7 +92,7 @@ func (a AppRepo) Page(page, size int, opts ...DBOption) (int64, []model.App, err
var apps []model.App
db := getDb(opts...).Model(&model.App{})
count := int64(0)
db = db.Count(&count)
db = db.Count(&count).Debug()
err := db.Limit(size).Offset(size * (page - 1)).Preload("AppTags").Find(&apps).Error
return count, apps, err
}
@ -137,3 +138,7 @@ func (a AppRepo) Save(ctx context.Context, app *model.App) error {
func (a AppRepo) BatchDelete(ctx context.Context, apps []model.App) error {
return getTx(ctx).Omit(clause.Associations).Delete(&apps).Error
}
func (a AppRepo) DeleteByIDs(ctx context.Context, ids []uint) error {
return getTx(ctx).Where("id in (?)", ids).Delete(&model.App{}).Error
}

View file

@ -19,6 +19,7 @@ type IAppDetailRepo interface {
Update(ctx context.Context, detail model.AppDetail) error
BatchCreate(ctx context.Context, details []model.AppDetail) error
DeleteByAppIds(ctx context.Context, appIds []uint) error
DeleteByIDs(ctx context.Context, appIds []uint) error
GetBy(opts ...DBOption) ([]model.AppDetail, error)
BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error
BatchDelete(ctx context.Context, appDetails []model.AppDetail) error
@ -64,6 +65,10 @@ func (a AppDetailRepo) DeleteByAppIds(ctx context.Context, appIds []uint) error
return getTx(ctx).Where("app_id in (?)", appIds).Delete(&model.AppDetail{}).Error
}
func (a AppDetailRepo) DeleteByIDs(ctx context.Context, appIds []uint) error {
return getTx(ctx).Where("id in (?)", appIds).Delete(&model.AppDetail{}).Error
}
func (a AppDetailRepo) GetBy(opts ...DBOption) ([]model.AppDetail, error) {
var details []model.AppDetail
err := getDb(opts...).Find(&details).Error

View file

@ -2,6 +2,7 @@ package repo
import (
"context"
"gorm.io/gorm"
"github.com/1Panel-dev/1Panel/agent/app/model"
)
@ -15,12 +16,21 @@ type IAppTagRepo interface {
DeleteAll(ctx context.Context) error
GetByAppId(appId uint) ([]model.AppTag, error)
GetByTagIds(tagIds []uint) ([]model.AppTag, error)
DeleteBy(ctx context.Context, opts ...DBOption) error
WithByTagID(tagID uint) DBOption
}
func NewIAppTagRepo() IAppTagRepo {
return &AppTagRepo{}
}
func (a AppTagRepo) WithByTagID(tagID uint) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("tag_id = ?", tagID)
}
}
func (a AppTagRepo) BatchCreate(ctx context.Context, tags []*model.AppTag) error {
return getTx(ctx).Create(&tags).Error
}
@ -48,3 +58,7 @@ func (a AppTagRepo) GetByTagIds(tagIds []uint) ([]model.AppTag, error) {
}
return appTags, nil
}
func (a AppTagRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.AppTag{}).Error
}

View file

@ -16,6 +16,9 @@ type ITagRepo interface {
GetByIds(ids []uint) ([]model.Tag, error)
GetByKeys(keys []string) ([]model.Tag, error)
GetByAppId(appId uint) ([]model.Tag, error)
DeleteByID(ctx context.Context, id uint) error
Create(ctx context.Context, tag *model.Tag) error
Save(ctx context.Context, tag *model.Tag) error
}
func NewITagRepo() ITagRepo {
@ -61,3 +64,15 @@ func (t TagRepo) GetByAppId(appId uint) ([]model.Tag, error) {
}
return tags, nil
}
func (t TagRepo) DeleteByID(ctx context.Context, id uint) error {
return getTx(ctx).Where("id = ?", id).Delete(&model.Tag{}).Error
}
func (t TagRepo) Create(ctx context.Context, tag *model.Tag) error {
return getTx(ctx).Create(tag).Error
}
func (t TagRepo) Save(ctx context.Context, tag *model.Tag) error {
return getTx(ctx).Save(tag).Error
}

View file

@ -120,6 +120,7 @@ func (a AppService) PageApp(req request.AppSearch) (interface{}, error) {
Website: ap.Website,
Github: ap.Github,
GpuSupport: ap.GpuSupport,
Recommend: ap.Recommend,
}
appDTOs = append(appDTOs, appDTO)
appTags, err := appTagRepo.GetByAppId(ap.ID)

View file

@ -7,6 +7,7 @@ import (
"fmt"
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"log"
"math"
"net/http"
@ -403,27 +404,6 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
}
switch install.App.Key {
case constant.AppOpenresty:
//TODO 删除 Openresty 不再删除网站
//websites, _ := websiteRepo.List()
//for _, website := range websites {
// if website.AppInstallID > 0 {
// websiteAppInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID))
// if websiteAppInstall.AppId > 0 {
// websiteApp, _ := appRepo.GetFirst(commonRepo.WithByID(websiteAppInstall.AppId))
// if websiteApp.Type == constant.RuntimePHP {
// go func() {
// _, _ = compose.Down(websiteAppInstall.GetComposePath())
// _ = op.DeleteDir(websiteAppInstall.GetPath())
// }()
// _ = appInstallRepo.Delete(ctx, websiteAppInstall)
// }
// }
// }
//}
//_ = websiteRepo.DeleteAll(ctx)
//_ = websiteDomainRepo.DeleteAll(ctx)
//xpack.RemoveTamper("")
case constant.AppMysql, constant.AppMariaDB:
_ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name))
case constant.AppPostgresql:
@ -1045,7 +1025,15 @@ func upApp(task *task.Task, appInstall *model.AppInstall, pullImages bool) {
if err != nil {
return err
}
imagePrefix := xpack.GetImagePrefix()
for _, image := range images {
if imagePrefix != "" {
lastSlashIndex := strings.LastIndex(image, "/")
if lastSlashIndex != -1 {
image = image[lastSlashIndex+1:]
}
image = imagePrefix + "/" + image
}
task.Log(i18n.GetWithName("PullImageStart", image))
if out, err = cmd.ExecWithTimeOut("docker pull "+image, 20*time.Minute); err != nil {
if out != "" {
@ -1555,6 +1543,22 @@ func addDockerComposeCommonParam(composeMap map[string]interface{}, serviceName
if !serviceValid {
return buserr.New(constant.ErrFileParse)
}
imagePreFix := xpack.GetImagePrefix()
if imagePreFix != "" {
for _, service := range services {
serviceValue := service.(map[string]interface{})
if image, ok := serviceValue["image"]; ok {
imageStr := image.(string)
lastSlashIndex := strings.LastIndex(imageStr, "/")
if lastSlashIndex != -1 {
imageStr = imageStr[lastSlashIndex+1:]
}
imageStr = imagePreFix + "/" + imageStr
serviceValue["image"] = imageStr
}
}
}
service, serviceExist := services[serviceName]
if !serviceExist {
return buserr.New(constant.ErrFileParse)

View file

@ -70,7 +70,11 @@ MoveSiteDir: "The current upgrade requires migrating the OpenResty website direc
MoveSiteToDir: "Migrate the website directory to {{ .name }}"
ErrMoveSiteDir: "Failed to migrate the website directory"
MoveSiteDirSuccess: "Successfully migrated the website directory"
DeleteRuntimePHP: "Delete PHP runtime environment"
DeleteRuntimePHP: "Delete PHP runtime environment",
CustomAppStoreNotConfig: "Please configure the offline package address in the app store settings",
CustomAppStoreNotFound: "Failed to retrieve app store package, please check if it exists",
CustomAppStoreFileValid: "App store package must be in .tar.gz format"
#file
ErrFileCanNotRead: "File can not read"

View file

@ -71,7 +71,11 @@ MoveSiteDir: "當前升級需要遷移 OpenResty 網站目錄"
MoveSiteToDir: "遷移網站目錄到 {{ .name }}"
ErrMoveSiteDir: "遷移網站目錄失敗"
MoveSiteDirSuccess: "遷移網站目錄成功"
DeleteRuntimePHP: "刪除運行環境 PHP 版本"
DeleteRuntimePHP: "刪除運行環境 PHP 版本",
CustomAppStoreNotConfig: "請在應用商店設置離線包地址",
CustomAppStoreNotFound: "應用商店包獲取失敗,請檢查是否存在",
CustomAppStoreFileValid: "應用商店包需要 .tar.gz 格式"
#file
ErrFileCanNotRead: "此文件不支持預覽"

View file

@ -71,6 +71,9 @@ MoveSiteToDir: "迁移网站目录到 {{ .name }}"
ErrMoveSiteDir: "迁移网站目录失败"
MoveSiteDirSuccess: "迁移网站目录成功"
DeleteRuntimePHP: "删除 PHP 运行环境"
CustomAppStoreNotConfig: "请在应用商店设置离线包地址"
CustomAppStoreNotFound: "应用商店包获取失败,请检查是否存在"
CustomAppStoreFileValid: "应用商店包需要 .tar.gz 格式"
#file
ErrFileCanNotRead: "此文件不支持预览"

View file

@ -43,3 +43,7 @@ func InitNodeData(tx *gorm.DB) (bool, string, error) { return true, "127.0.0.1",
func RequestToMaster(reqUrl, reqMethod string, reqBody io.Reader) (interface{}, error) {
return nil, nil
}
func GetImagePrefix() string {
return ""
}

View file

@ -118,3 +118,7 @@ export const GetAppStoreConfig = () => {
export const UpdateAppStoreConfig = (req: App.AppStoreConfig) => {
return http.post(`apps/store/update`, req);
};
export const SyncCutomAppStore = (req: App.AppStoreSync) => {
return http.post(`/custom/app/sync`, req);
};

View file

@ -175,7 +175,7 @@
<script lang="ts" setup>
import { App } from '@/api/interface/app';
import { onMounted, reactive, ref, computed } from 'vue';
import { GetAppTags, SearchApp, SyncApp, SyncLocalApp } from '@/api/modules/app';
import { GetAppTags, SearchApp, SyncApp, SyncCutomAppStore, SyncLocalApp } from '@/api/modules/app';
import Install from '../detail/install/index.vue';
import router from '@/routers';
import { MsgSuccess } from '@/utils/message';
@ -183,8 +183,11 @@ import { GlobalStore } from '@/store';
import { getLanguage, newUUID } from '@/utils/util';
import Detail from '../detail/index.vue';
import TaskLog from '@/components/task-log/index.vue';
import { storeToRefs } from 'pinia';
import { GetCustomAppStoreConfig } from '@/xpack/api/modules/app';
const globalStore = GlobalStore();
const { isProductPro } = storeToRefs(globalStore);
const mobile = computed(() => {
return globalStore.isMobile();
@ -221,6 +224,7 @@ const moreTag = ref('');
const mainHeight = ref(0);
const detailRef = ref();
const taskLogRef = ref();
const syncCustomAppstore = ref(false);
const search = async (req: App.AppReq) => {
loading.value = true;
@ -265,25 +269,29 @@ const openTaskLog = (taskID: string) => {
taskLogRef.value.openWithTaskID(taskID);
};
const sync = () => {
const sync = async () => {
syncing.value = true;
const taskID = newUUID();
const syncReq = {
taskID: taskID,
};
SyncApp(syncReq)
.then((res) => {
if (res.message != '') {
MsgSuccess(res.message);
} else {
openTaskLog(taskID);
}
canUpdate.value = false;
search(req);
})
.finally(() => {
syncing.value = false;
});
try {
let res;
if (isProductPro.value && syncCustomAppstore.value) {
res = await SyncCutomAppStore(syncReq);
} else {
res = await SyncApp(syncReq);
}
if (res.message != '') {
MsgSuccess(res.message);
} else {
openTaskLog(taskID);
}
canUpdate.value = false;
search(req);
} finally {
syncing.value = false;
}
};
const syncLocal = () => {
@ -329,7 +337,7 @@ const searchByName = () => {
search(req);
};
onMounted(() => {
onMounted(async () => {
if (router.currentRoute.value.query.install) {
installKey.value = String(router.currentRoute.value.query.install);
const params = {
@ -340,6 +348,12 @@ onMounted(() => {
installRef.value.acceptParams(params);
}
search(req);
if (isProductPro.value) {
const res = await GetCustomAppStoreConfig();
if (res && res.data) {
syncCustomAppstore.value = res.data.status === 'enable';
}
}
mainHeight.value = window.innerHeight - 380;
window.onresize = () => {
return (() => {

View file

@ -0,0 +1,87 @@
<template>
<DrawerPro v-model="drawerVisible" :header="$t('app.defaultWebDomain')" :back="handleClose" size="small">
<el-form ref="formRef" label-position="top" :model="form" :rules="rules" @submit.prevent v-loading="loading">
<el-form-item :label="$t('app.defaultWebDomain')" prop="defaultDomain">
<el-input v-model="form.defaultDomain">
<template #prepend>
<el-select v-model="protocol" placeholder="Select" class="p-w-100">
<el-option label="HTTP" value="http://" />
<el-option label="HTTPS" value="https://" />
</el-select>
</template>
</el-input>
<span class="input-help">{{ $t('app.defaultWebDomainHepler') }}</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose()">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="submit()">
{{ $t('commons.button.confirm') }}
</el-button>
</template>
</DrawerPro>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import i18n from '@/lang';
import { FormInstance } from 'element-plus';
import { Rules } from '@/global/form-rules';
import { UpdateAppStoreConfig } from '@/api/modules/app';
import { MsgSuccess } from '@/utils/message';
const emit = defineEmits<{ (e: 'close'): void }>();
const drawerVisible = ref();
const loading = ref();
const form = reactive({
defaultDomain: '',
});
const rules = reactive({
defaultDomain: [Rules.requiredInput],
});
const formRef = ref<FormInstance>();
const protocol = ref('http://');
interface DialogProps {
protocol: string;
domain: string;
}
const acceptParams = (config: DialogProps): void => {
form.defaultDomain = config.domain;
protocol.value;
drawerVisible.value = true;
};
const handleClose = () => {
drawerVisible.value = false;
emit('close');
};
const submit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (!valid) {
return;
}
loading.value = true;
try {
let defaultDomain = '';
if (form.defaultDomain) {
defaultDomain = protocol.value + form.defaultDomain;
}
const req = {
defaultDomain: defaultDomain,
};
await UpdateAppStoreConfig(req);
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
} catch (error) {
} finally {
loading.value = false;
handleClose();
}
});
};
defineExpose({
acceptParams,
});
</script>

View file

@ -4,7 +4,7 @@
<el-form
:model="config"
label-position="left"
label-width="150px"
label-width="180px"
class="ml-2.5"
v-loading="loading"
:rules="rules"
@ -15,32 +15,39 @@
<el-form-item :label="$t('app.defaultWebDomain')" prop="defaultDomain">
<el-input v-model="config.defaultDomain">
<template #prepend>
<el-select v-model="protocol" placeholder="Select" class="p-w-100">
<el-select v-model="protocol" placeholder="Select" class="p-w-100" disabled>
<el-option label="HTTP" value="http://" />
<el-option label="HTTPS" value="https://" />
</el-select>
</template>
<template #append>
<el-button @click="setDefaultDomain()" icon="Setting">
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
<span class="input-help">{{ $t('app.defaultWebDomainHepler') }}</span>
</el-form-item>
<el-form-item>
<el-button type="primary" :disabled="loading" @click="submit()">
{{ $t('commons.button.confirm') }}
</el-button>
</el-form-item>
<CustomSetting v-if="isProductPro" />
</el-col>
</el-row>
</el-form>
</template>
</LayoutContent>
<DefaultDomain ref="domainRef" @close="search" />
</template>
<script setup lang="ts">
import { GetAppStoreConfig, UpdateAppStoreConfig } from '@/api/modules/app';
import { GetAppStoreConfig } from '@/api/modules/app';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormRules } from 'element-plus';
import CustomSetting from '@/xpack/views/appstore/index.vue';
import DefaultDomain from './default-domain/index.vue';
import { GlobalStore } from '@/store';
import { storeToRefs } from 'pinia';
const globalStore = GlobalStore();
const { isProductPro } = storeToRefs(globalStore);
const rules = ref<FormRules>({
defaultDomain: [Rules.domainOrIP],
@ -51,12 +58,11 @@ const config = ref({
const loading = ref(false);
const configForm = ref();
const protocol = ref('http://');
const domainRef = ref();
function getUrl(url: string) {
const regex = /^(https?:\/\/)(.*)/;
const match = url.match(regex);
if (match) {
const protocol = match[1];
const remainder = match[2];
@ -86,28 +92,10 @@ const search = async () => {
}
};
const submit = async () => {
if (!configForm.value) return;
await configForm.value.validate(async (valid) => {
if (!valid) {
return;
}
loading.value = true;
try {
let defaultDomain = '';
if (config.value.defaultDomain) {
defaultDomain = protocol.value + config.value.defaultDomain;
}
const req = {
defaultDomain: defaultDomain,
};
await UpdateAppStoreConfig(req);
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
} catch (error) {
} finally {
loading.value = false;
search();
}
const setDefaultDomain = () => {
domainRef.value.acceptParams({
domain: config.value.defaultDomain,
protocol: protocol.value,
});
};