mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-03-13 17:24:44 +08:00
feat: merge from dev (#7366)
* feat: merge from dev * feat: Merge mobile style code from the dev branch
This commit is contained in:
parent
a8170a499a
commit
a627aa9915
@ -171,7 +171,7 @@ func (b *BaseApi) GetIgnoredApp(c *gin.Context) {
|
||||
// @Success 200 {object} model.AppInstall
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /apps/install [post]
|
||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[{"input_column":"name","input_value":"name","isList":false,"db":"app_installs","output_column":"app_id","output_value":"appId"},{"info":"appId","isList":false,"db":"apps","output_column":"key","output_value":"appKey"}],"formatZH":"安装应用 [appKey]-[name]","formatEN":"Install app [appKey]-[name]"}
|
||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"安装应用 [name]","formatEN":"Install app [name]"}
|
||||
func (b *BaseApi) InstallApp(c *gin.Context) {
|
||||
var req request.AppInstallCreate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
|
@ -93,7 +93,7 @@ func (b *BaseApi) SearchJobRecords(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
loc, _ := time.LoadLocation(common.LoadTimeZone())
|
||||
loc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
req.StartTime = req.StartTime.In(loc)
|
||||
req.EndTime = req.EndTime.In(loc)
|
||||
|
||||
|
@ -61,7 +61,7 @@ func (b *BaseApi) CreateRuntime(c *gin.Context) {
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /runtimes/del [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除网站 [name]","formatEN":"Delete website [name]"}
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除运行环境 [name]","formatEN":"Delete runtime [name]"}
|
||||
func (b *BaseApi) DeleteRuntime(c *gin.Context) {
|
||||
var req request.RuntimeDelete
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
|
@ -113,12 +113,12 @@ func (b *BaseApi) UpdateSnapDescription(c *gin.Context) {
|
||||
// @Summary Page system snapshot
|
||||
// @Description 获取系统快照列表分页
|
||||
// @Accept json
|
||||
// @Param request body dto.SearchWithPage true "request"
|
||||
// @Param request body dto.PageSnapshot true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /settings/snapshot/search [post]
|
||||
func (b *BaseApi) SearchSnapshot(c *gin.Context) {
|
||||
var req dto.SearchWithPage
|
||||
var req dto.PageSnapshot
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
type SearchClamWithPage struct {
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name status created_at"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name status createdAt"`
|
||||
Order string `json:"order" validate:"required,oneof=null ascending descending"`
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ package dto
|
||||
|
||||
type SearchCommandWithPage struct {
|
||||
PageInfo
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name command created_at"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name command createdAt"`
|
||||
Order string `json:"order" validate:"required,oneof=null ascending descending"`
|
||||
GroupID uint `json:"groupID"`
|
||||
Info string `json:"info"`
|
||||
|
@ -8,7 +8,7 @@ type PageContainer struct {
|
||||
PageInfo
|
||||
Name string `json:"name"`
|
||||
State string `json:"state" validate:"required,oneof=all created running paused restarting removing exited dead"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name created_at"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name createdAt"`
|
||||
Order string `json:"order" validate:"required,oneof=null ascending descending"`
|
||||
Filters string `json:"filters"`
|
||||
ExcludeAppStore bool `json:"excludeAppStore"`
|
||||
@ -148,7 +148,7 @@ type PortHelper struct {
|
||||
|
||||
type ContainerOperation struct {
|
||||
Names []string `json:"names" validate:"required"`
|
||||
Operation string `json:"operation" validate:"required,oneof=start stop restart kill pause unpause remove"`
|
||||
Operation string `json:"operation" validate:"required,oneof=up start stop restart kill pause unpause remove"`
|
||||
}
|
||||
|
||||
type ContainerRename struct {
|
||||
@ -232,6 +232,7 @@ type ComposeInfo struct {
|
||||
Workdir string `json:"workdir"`
|
||||
Path string `json:"path"`
|
||||
Containers []ComposeContainer `json:"containers"`
|
||||
Env []string `json:"env"`
|
||||
}
|
||||
type ComposeContainer struct {
|
||||
ContainerID string `json:"containerID"`
|
||||
@ -240,23 +241,25 @@ type ComposeContainer struct {
|
||||
State string `json:"state"`
|
||||
}
|
||||
type ComposeCreate struct {
|
||||
TaskID string `json:"taskID"`
|
||||
Name string `json:"name"`
|
||||
From string `json:"from" validate:"required,oneof=edit path template"`
|
||||
File string `json:"file"`
|
||||
Path string `json:"path"`
|
||||
Template uint `json:"template"`
|
||||
TaskID string `json:"taskID"`
|
||||
Name string `json:"name"`
|
||||
From string `json:"from" validate:"required,oneof=edit path template"`
|
||||
File string `json:"file"`
|
||||
Path string `json:"path"`
|
||||
Template uint `json:"template"`
|
||||
Env []string `json:"env"`
|
||||
}
|
||||
type ComposeOperation struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
Operation string `json:"operation" validate:"required,oneof=start stop down"`
|
||||
Path string `json:"path"`
|
||||
Operation string `json:"operation" validate:"required,oneof=up start stop down delete"`
|
||||
WithFile bool `json:"withFile"`
|
||||
}
|
||||
type ComposeUpdate struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
Content string `json:"content" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
Content string `json:"content" validate:"required"`
|
||||
Env []string `json:"env"`
|
||||
}
|
||||
|
||||
type ContainerLog struct {
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
type PageCronjob struct {
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name status created_at"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name status createdAt"`
|
||||
Order string `json:"order" validate:"required,oneof=null ascending descending"`
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ type MysqlDBSearch struct {
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name created_at"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name createdAt"`
|
||||
Order string `json:"order" validate:"required,oneof=null ascending descending"`
|
||||
}
|
||||
|
||||
@ -236,7 +236,7 @@ type DatabaseSearch struct {
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
Type string `json:"type"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name created_at"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name createdAt"`
|
||||
Order string `json:"order" validate:"required,oneof=null ascending descending"`
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ type PostgresqlDBSearch struct {
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name created_at"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name createdAt"`
|
||||
Order string `json:"order" validate:"required,oneof=null ascending descending"`
|
||||
}
|
||||
|
||||
|
@ -2,54 +2,8 @@ package dto
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
type OperationLog struct {
|
||||
ID uint `json:"id"`
|
||||
Source string `json:"source"`
|
||||
|
||||
IP string `json:"ip"`
|
||||
Path string `json:"path"`
|
||||
Method string `json:"method"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
|
||||
Latency time.Duration `json:"latency"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
|
||||
DetailZH string `json:"detailZH"`
|
||||
DetailEN string `json:"detailEN"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type SearchOpLogWithPage struct {
|
||||
PageInfo
|
||||
Source string `json:"source"`
|
||||
Status string `json:"status"`
|
||||
Operation string `json:"operation"`
|
||||
}
|
||||
|
||||
type SearchLgLogWithPage struct {
|
||||
PageInfo
|
||||
IP string `json:"ip"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type LoginLog struct {
|
||||
ID uint `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
Address string `json:"address"`
|
||||
Agent string `json:"agent"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type CleanLog struct {
|
||||
LogType string `json:"logType" validate:"required,oneof=login operation"`
|
||||
}
|
||||
|
||||
type SearchTaskLogReq struct {
|
||||
Status string `json:"status"`
|
||||
Type string `json:"type"`
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
type WebsiteSearch struct {
|
||||
dto.PageInfo
|
||||
Name string `json:"name"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=primary_domain type status created_at expire_date"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=primary_domain type status createdAt expire_date"`
|
||||
Order string `json:"order" validate:"required,oneof=null ascending descending"`
|
||||
WebsiteGroupID uint `json:"websiteGroupId"`
|
||||
}
|
||||
|
@ -13,6 +13,13 @@ type SnapshotStatus struct {
|
||||
Upload string `json:"upload"`
|
||||
}
|
||||
|
||||
type PageSnapshot struct {
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name createdAt"`
|
||||
Order string `json:"order" validate:"required,oneof=null ascending descending"`
|
||||
}
|
||||
|
||||
type SnapshotCreate struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
@ -12,4 +12,5 @@ type Compose struct {
|
||||
BaseModel
|
||||
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
@ -113,11 +113,17 @@ func (c *CommonRepo) WithByCreatedAt(startTime, endTime time.Time) DBOption {
|
||||
}
|
||||
|
||||
func (c *CommonRepo) WithOrderBy(orderStr string) DBOption {
|
||||
if orderStr == "createdAt" {
|
||||
orderStr = "created_at"
|
||||
}
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Order(orderStr)
|
||||
}
|
||||
}
|
||||
func (c *CommonRepo) WithOrderRuleBy(orderBy, order string) DBOption {
|
||||
if orderBy == "createdAt" {
|
||||
orderBy = "created_at"
|
||||
}
|
||||
switch order {
|
||||
case constant.OrderDesc:
|
||||
order = "desc"
|
||||
|
@ -19,6 +19,7 @@ type IComposeTemplateRepo interface {
|
||||
CreateRecord(compose *model.Compose) error
|
||||
DeleteRecord(opts ...DBOption) error
|
||||
ListRecord() ([]model.Compose, error)
|
||||
UpdateRecord(name string, vars map[string]interface{}) error
|
||||
}
|
||||
|
||||
func NewIComposeTemplateRepo() IComposeTemplateRepo {
|
||||
@ -102,3 +103,6 @@ func (u *ComposeTemplateRepo) DeleteRecord(opts ...DBOption) error {
|
||||
}
|
||||
return db.Delete(&model.Compose{}).Error
|
||||
}
|
||||
func (u *ComposeTemplateRepo) UpdateRecord(name string, vars map[string]interface{}) error {
|
||||
return global.DB.Model(&model.Compose{}).Where("name = ?", name).Updates(vars).Error
|
||||
}
|
||||
|
@ -92,6 +92,8 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
|
||||
baseInfo.Version = strings.TrimPrefix(version, "ClamAV ")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_ = StopAllCronJob(false)
|
||||
}
|
||||
if baseInfo.FreshIsActive {
|
||||
version, err := cmd.Exec("freshclam --version")
|
||||
@ -139,7 +141,7 @@ func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interfa
|
||||
item.LastHandleDate = "-"
|
||||
datas = append(datas, item)
|
||||
}
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZone())
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
for i := 0; i < len(datas); i++ {
|
||||
logPaths := loadFileByName(datas[i].Name)
|
||||
sort.Slice(logPaths, func(i, j int) bool {
|
||||
@ -268,7 +270,7 @@ func (c *ClamService) Delete(req dto.ClamDelete) error {
|
||||
}
|
||||
|
||||
func (c *ClamService) HandleOnce(req dto.OperateByID) error {
|
||||
if !cmd.Which("clamdscan") {
|
||||
if cleaned := StopAllCronJob(true); cleaned {
|
||||
return buserr.New("ErrClamdscanNotFound")
|
||||
}
|
||||
clam, _ := clamRepo.Get(commonRepo.WithByID(req.ID))
|
||||
@ -321,7 +323,7 @@ func (c *ClamService) LoadRecords(req dto.ClamLogSearch) (int64, interface{}, er
|
||||
}
|
||||
|
||||
var filterFiles []string
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZone())
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
for _, item := range logPaths {
|
||||
t1, err := time.ParseInLocation(constant.DateTimeSlimLayout, item, nyc)
|
||||
if err != nil {
|
||||
@ -473,6 +475,29 @@ func (c *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func StopAllCronJob(withCheck bool) bool {
|
||||
if withCheck {
|
||||
isActive := false
|
||||
exist1, _ := systemctl.IsExist(clamServiceNameCentOs)
|
||||
if exist1 {
|
||||
isActive, _ = systemctl.IsActive(clamServiceNameCentOs)
|
||||
}
|
||||
exist2, _ := systemctl.IsExist(clamServiceNameUbuntu)
|
||||
if exist2 {
|
||||
isActive, _ = systemctl.IsActive(clamServiceNameUbuntu)
|
||||
}
|
||||
if isActive {
|
||||
return false
|
||||
}
|
||||
}
|
||||
clams, _ := clamRepo.List(commonRepo.WithByStatus(constant.StatusEnable))
|
||||
for i := 0; i < len(clams); i++ {
|
||||
global.Cron.Remove(cron.EntryID(clams[i].EntryID))
|
||||
_ = clamRepo.Update(clams[i].ID, map[string]interface{}{"status": constant.StatusDisable, "entry_id": 0})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func loadFileByName(name string) []string {
|
||||
var logPaths []string
|
||||
pathItem := path.Join(global.CONF.System.DataDir, resultDir, name)
|
||||
|
@ -186,7 +186,7 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
|
||||
IsFromApp = true
|
||||
}
|
||||
|
||||
ports := loadContainerPort(item.Ports)
|
||||
exposePorts := transPortToStr(records[i].Ports)
|
||||
info := dto.ContainerInfo{
|
||||
ContainerID: item.ID,
|
||||
CreateTime: time.Unix(item.Created, 0).Format(constant.DateTimeLayout),
|
||||
@ -195,7 +195,7 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro
|
||||
ImageName: item.Image,
|
||||
State: item.State,
|
||||
RunTime: item.Status,
|
||||
Ports: ports,
|
||||
Ports: exposePorts,
|
||||
IsFromApp: IsFromApp,
|
||||
IsFromCompose: IsFromCompose,
|
||||
}
|
||||
@ -553,6 +553,8 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
|
||||
}
|
||||
}
|
||||
|
||||
exposePorts, _ := loadPortByInspect(oldContainer.ID, client)
|
||||
data.ExposedPorts = loadContainerPortForInfo(exposePorts)
|
||||
networkSettings := oldContainer.NetworkSettings
|
||||
bridgeNetworkSettings := networkSettings.Networks[data.Network]
|
||||
if bridgeNetworkSettings.IPAMConfig != nil {
|
||||
@ -579,19 +581,6 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
|
||||
for key, val := range oldContainer.Config.Labels {
|
||||
data.Labels = append(data.Labels, fmt.Sprintf("%s=%s", key, val))
|
||||
}
|
||||
for key, val := range oldContainer.HostConfig.PortBindings {
|
||||
var itemPort dto.PortHelper
|
||||
if !strings.Contains(string(key), "/") {
|
||||
continue
|
||||
}
|
||||
itemPort.ContainerPort = strings.Split(string(key), "/")[0]
|
||||
itemPort.Protocol = strings.Split(string(key), "/")[1]
|
||||
for _, binds := range val {
|
||||
itemPort.HostIP = binds.HostIP
|
||||
itemPort.HostPort = binds.HostPort
|
||||
data.ExposedPorts = append(data.ExposedPorts, itemPort)
|
||||
}
|
||||
}
|
||||
data.AutoRemove = oldContainer.HostConfig.AutoRemove
|
||||
data.Privileged = oldContainer.HostConfig.Privileged
|
||||
data.PublishAllPorts = oldContainer.HostConfig.PublishAllPorts
|
||||
@ -1021,6 +1010,10 @@ func (u *ContainerService) LoadContainerLogs(req dto.OperationWithNameAndType) s
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
composeItem, _ := composeRepo.GetRecord(commonRepo.WithByName(req.Name))
|
||||
filePath = composeItem.Path
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
return ""
|
||||
@ -1109,22 +1102,6 @@ func checkImageExist(client *client.Client, imageItem string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func checkImage(client *client.Client, imageItem string) bool {
|
||||
images, err := client.ImageList(context.Background(), image.ListOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, img := range images {
|
||||
for _, tag := range img.RepoTags {
|
||||
if tag == imageItem || tag == imageItem+":latest" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func pullImages(ctx context.Context, client *client.Client, imageName string) error {
|
||||
options := image.PullOptions{}
|
||||
repos, _ := imageRepoRepo.List()
|
||||
@ -1340,7 +1317,7 @@ func reCreateAfterUpdate(name string, client *client.Client, config *container.C
|
||||
if err := client.ContainerStart(ctx, oldContainer.ID, container.StartOptions{}); err != nil {
|
||||
global.LOG.Errorf("restart after container update failed, err: %v", err)
|
||||
}
|
||||
global.LOG.Errorf("recreate after container update successful")
|
||||
global.LOG.Info("recreate after container update successful")
|
||||
}
|
||||
|
||||
func loadVolumeBinds(binds []types.MountPoint) []dto.VolumeHelper {
|
||||
@ -1363,7 +1340,27 @@ func loadVolumeBinds(binds []types.MountPoint) []dto.VolumeHelper {
|
||||
return datas
|
||||
}
|
||||
|
||||
func loadContainerPort(ports []types.Port) []string {
|
||||
func loadPortByInspect(id string, client *client.Client) ([]types.Port, error) {
|
||||
container, err := client.ContainerInspect(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var itemPorts []types.Port
|
||||
for key, val := range container.ContainerJSONBase.HostConfig.PortBindings {
|
||||
if !strings.Contains(string(key), "/") {
|
||||
continue
|
||||
}
|
||||
item := strings.Split(string(key), "/")
|
||||
itemPort, _ := strconv.ParseUint(item[0], 10, 16)
|
||||
|
||||
for _, itemVal := range val {
|
||||
publicPort, _ := strconv.ParseUint(itemVal.HostPort, 10, 16)
|
||||
itemPorts = append(itemPorts, types.Port{PrivatePort: uint16(itemPort), Type: item[1], PublicPort: uint16(publicPort), IP: itemVal.HostIP})
|
||||
}
|
||||
}
|
||||
return itemPorts, nil
|
||||
}
|
||||
func transPortToStr(ports []types.Port) []string {
|
||||
var (
|
||||
ipv4Ports []types.Port
|
||||
ipv6Ports []types.Port
|
||||
@ -1475,3 +1472,39 @@ func loadComposeCount(client *client.Client) int {
|
||||
|
||||
return len(composeMap)
|
||||
}
|
||||
func loadContainerPortForInfo(itemPorts []types.Port) []dto.PortHelper {
|
||||
var exposedPorts []dto.PortHelper
|
||||
samePortMap := make(map[string]dto.PortHelper)
|
||||
ports := transPortToStr(itemPorts)
|
||||
var itemPort dto.PortHelper
|
||||
for _, item := range ports {
|
||||
itemStr := strings.Split(item, "->")
|
||||
if len(itemStr) < 2 {
|
||||
continue
|
||||
}
|
||||
lastIndex := strings.LastIndex(itemStr[0], ":")
|
||||
if lastIndex == -1 {
|
||||
itemPort.HostPort = itemStr[0]
|
||||
} else {
|
||||
itemPort.HostIP = itemStr[0][0:lastIndex]
|
||||
itemPort.HostPort = itemStr[0][lastIndex+1:]
|
||||
}
|
||||
itemContainer := strings.Split(itemStr[1], "/")
|
||||
if len(itemContainer) != 2 {
|
||||
continue
|
||||
}
|
||||
itemPort.ContainerPort = itemContainer[0]
|
||||
itemPort.Protocol = itemContainer[1]
|
||||
keyItem := fmt.Sprintf("%s->%s/%s", itemPort.HostPort, itemPort.ContainerPort, itemPort.Protocol)
|
||||
if val, ok := samePortMap[keyItem]; ok {
|
||||
val.HostIP = ""
|
||||
samePortMap[keyItem] = val
|
||||
} else {
|
||||
samePortMap[keyItem] = itemPort
|
||||
}
|
||||
}
|
||||
for _, val := range samePortMap {
|
||||
exposedPorts = append(exposedPorts, val)
|
||||
}
|
||||
return exposedPorts
|
||||
}
|
||||
|
@ -53,6 +53,20 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
}
|
||||
|
||||
composeCreatedByLocal, _ := composeRepo.ListRecord()
|
||||
|
||||
composeLocalMap := make(map[string]dto.ComposeInfo)
|
||||
for _, localItem := range composeCreatedByLocal {
|
||||
composeItemLocal := dto.ComposeInfo{
|
||||
ContainerNumber: 0,
|
||||
CreatedAt: localItem.CreatedAt.Format(constant.DateTimeLayout),
|
||||
ConfigFile: localItem.Path,
|
||||
Workdir: strings.TrimSuffix(localItem.Path, "/docker-compose.yml"),
|
||||
}
|
||||
composeItemLocal.CreatedBy = "1Panel"
|
||||
composeItemLocal.Path = localItem.Path
|
||||
composeLocalMap[localItem.Name] = composeItemLocal
|
||||
}
|
||||
|
||||
composeMap := make(map[string]dto.ComposeInfo)
|
||||
for _, container := range list {
|
||||
if name, ok := container.Labels[composeProjectLabel]; ok {
|
||||
@ -96,12 +110,24 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, item := range composeCreatedByLocal {
|
||||
if err := composeRepo.DeleteRecord(commonRepo.WithByID(item.ID)); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
||||
mergedMap := make(map[string]dto.ComposeInfo)
|
||||
for key, localItem := range composeLocalMap {
|
||||
mergedMap[key] = localItem
|
||||
}
|
||||
for key, item := range composeMap {
|
||||
if existingItem, exists := mergedMap[key]; exists {
|
||||
if item.ContainerNumber > 0 {
|
||||
if existingItem.ContainerNumber <= 0 {
|
||||
mergedMap[key] = item
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mergedMap[key] = item
|
||||
}
|
||||
}
|
||||
for key, value := range composeMap {
|
||||
|
||||
for key, value := range mergedMap {
|
||||
value.Name = key
|
||||
records = append(records, value)
|
||||
}
|
||||
@ -128,7 +154,8 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
||||
}
|
||||
BackDatas = records[start:end]
|
||||
}
|
||||
return int64(total), BackDatas, nil
|
||||
listItem := loadEnv(BackDatas)
|
||||
return int64(total), listItem, nil
|
||||
}
|
||||
|
||||
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
||||
@ -142,6 +169,9 @@ func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
||||
if err := u.loadPath(&req); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
||||
return false, err
|
||||
}
|
||||
cmd := exec.Command("docker", "compose", "-f", req.Path, "config")
|
||||
stdout, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
@ -164,6 +194,9 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("new task for image build failed, err: %v", err)
|
||||
}
|
||||
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(i18n.GetMsgByKey("ComposeCreate"), func(t *task.Task) error {
|
||||
cmd := exec.Command("docker-compose", "-f", req.Path, "up", "-d")
|
||||
@ -173,7 +206,7 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
||||
_, _ = compose.Down(req.Path)
|
||||
return err
|
||||
}
|
||||
_ = composeRepo.CreateRecord(&model.Compose{Name: req.Name})
|
||||
_ = composeRepo.CreateRecord(&model.Compose{Name: req.Name, Path: req.Path})
|
||||
return nil
|
||||
}, nil)
|
||||
_ = taskItem.Execute()
|
||||
@ -183,23 +216,43 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
||||
}
|
||||
|
||||
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
||||
if len(req.Path) == 0 && req.Operation == "delete" {
|
||||
_ = composeRepo.DeleteRecord(commonRepo.WithByName(req.Name))
|
||||
return nil
|
||||
}
|
||||
if cmd.CheckIllegal(req.Path, req.Operation) {
|
||||
return buserr.New(constant.ErrCmdIllegal)
|
||||
}
|
||||
if _, err := os.Stat(req.Path); err != nil {
|
||||
return fmt.Errorf("load file with path %s failed, %v", req.Path, err)
|
||||
}
|
||||
if stdout, err := compose.Operate(req.Path, req.Operation); err != nil {
|
||||
return errors.New(string(stdout))
|
||||
}
|
||||
global.LOG.Infof("docker-compose %s %s successful", req.Operation, req.Name)
|
||||
if req.Operation == "down" {
|
||||
_ = composeRepo.DeleteRecord(commonRepo.WithByName(req.Name))
|
||||
if req.Operation == "delete" {
|
||||
if stdout, err := compose.Operate(req.Path, "down"); err != nil {
|
||||
return errors.New(string(stdout))
|
||||
}
|
||||
if req.WithFile {
|
||||
_ = composeRepo.DeleteRecord(commonRepo.WithByName(req.Name))
|
||||
_ = os.RemoveAll(path.Dir(req.Path))
|
||||
} else {
|
||||
composeItem, _ := composeRepo.GetRecord(commonRepo.WithByName(req.Name))
|
||||
if composeItem.Path == "" {
|
||||
upMap := make(map[string]interface{})
|
||||
upMap["path"] = req.Path
|
||||
_ = composeRepo.UpdateRecord(req.Name, upMap)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if req.Operation == "up" {
|
||||
if stdout, err := compose.Up(req.Path); err != nil {
|
||||
return errors.New(string(stdout))
|
||||
}
|
||||
} else {
|
||||
if stdout, err := compose.Operate(req.Path, req.Operation); err != nil {
|
||||
return errors.New(string(stdout))
|
||||
}
|
||||
}
|
||||
|
||||
global.LOG.Infof("docker-compose %s %s successful", req.Operation, req.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -221,6 +274,10 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
||||
write.Flush()
|
||||
|
||||
global.LOG.Infof("docker-compose.yml %s has been replaced, now start to docker-compose restart", req.Path)
|
||||
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if stdout, err := compose.Up(req.Path); err != nil {
|
||||
if err := recreateCompose(string(oldFile), req.Path); err != nil {
|
||||
return fmt.Errorf("update failed when handle compose up, err: %s, recreate failed: %v", string(stdout), err)
|
||||
@ -269,3 +326,43 @@ func recreateCompose(content, path string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadEnv(list []dto.ComposeInfo) []dto.ComposeInfo {
|
||||
for i := 0; i < len(list); i++ {
|
||||
envFilePath := path.Join(path.Dir(list[i].Path), "1panel.env")
|
||||
file, err := os.ReadFile(envFilePath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
lines := strings.Split(string(file), "\n")
|
||||
for _, line := range lines {
|
||||
lineItem := strings.TrimSpace(line)
|
||||
if len(lineItem) != 0 && !strings.HasPrefix(lineItem, "#") {
|
||||
list[i].Env = append(list[i].Env, lineItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func newComposeEnv(pathItem string, env []string) error {
|
||||
if len(env) == 0 {
|
||||
return nil
|
||||
}
|
||||
envFilePath := path.Join(path.Dir(pathItem), "1panel.env")
|
||||
file, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("failed to create env file: %v", err)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
for _, env := range env {
|
||||
envItem := strings.TrimSpace(env)
|
||||
if _, err := file.WriteString(fmt.Sprintf("%s\n", envItem)); err != nil {
|
||||
global.LOG.Errorf("failed to write env to file: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
global.LOG.Infof("1panel.env file successfully created or updated with env variables in %s", envFilePath)
|
||||
return nil
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ func (u *ContainerService) PageVolume(req dto.SearchWithPage) (int64, interface{
|
||||
records = list.Volumes[start:end]
|
||||
}
|
||||
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZone())
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
for _, item := range records {
|
||||
tag := make([]string, 0)
|
||||
for _, val := range item.Labels {
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time) error {
|
||||
@ -28,6 +29,9 @@ func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time) e
|
||||
}
|
||||
apps = append(apps, app)
|
||||
}
|
||||
if len(apps) == 0 {
|
||||
return errors.New("no such app in database!")
|
||||
}
|
||||
accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
|
||||
if err != nil {
|
||||
return err
|
||||
@ -61,6 +65,9 @@ func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time) e
|
||||
|
||||
func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Time) error {
|
||||
webs := loadWebsForJob(cronjob)
|
||||
if len(webs) == 0 {
|
||||
return errors.New("no such website in database!")
|
||||
}
|
||||
accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
|
||||
if err != nil {
|
||||
return err
|
||||
@ -94,6 +101,9 @@ func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Tim
|
||||
|
||||
func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Time) error {
|
||||
dbs := loadDbsForJob(cronjob)
|
||||
if len(dbs) == 0 {
|
||||
return errors.New("no such db in database!")
|
||||
}
|
||||
accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
pathUtils "path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -104,8 +104,8 @@ func (u *CronjobService) handleShell(cronjob model.Cronjob, logPath string) erro
|
||||
cronjob.Executor = "bash"
|
||||
}
|
||||
if cronjob.ScriptMode == "input" {
|
||||
fileItem := path.Join(global.CONF.System.BaseDir, "1panel", "task", "shell", cronjob.Name, cronjob.Name+".sh")
|
||||
_ = os.MkdirAll(path.Dir(fileItem), os.ModePerm)
|
||||
fileItem := pathUtils.Join(global.CONF.System.BaseDir, "1panel", "task", "shell", cronjob.Name, cronjob.Name+".sh")
|
||||
_ = os.MkdirAll(pathUtils.Dir(fileItem), os.ModePerm)
|
||||
shellFile, err := os.OpenFile(fileItem, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -149,7 +149,6 @@ func handleTar(sourceDir, targetDir, name, exclusionRules string, secret string)
|
||||
|
||||
excludes := strings.Split(exclusionRules, ",")
|
||||
excludeRules := ""
|
||||
excludes = append(excludes, "*.sock")
|
||||
for _, exclude := range excludes {
|
||||
if len(exclude) == 0 {
|
||||
continue
|
||||
@ -172,10 +171,14 @@ func handleTar(sourceDir, targetDir, name, exclusionRules string, secret string)
|
||||
|
||||
if len(secret) != 0 {
|
||||
extraCmd := "| openssl enc -aes-256-cbc -salt -k '" + secret + "' -out"
|
||||
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s %s %s", " -"+excludeRules, path, extraCmd, targetDir+"/"+name)
|
||||
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -print) -zcf %s %s %s %s", sourceDir, " -"+excludeRules, path, extraCmd, targetDir+"/"+name)
|
||||
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
|
||||
} else {
|
||||
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s %s", targetDir+"/"+name, excludeRules, path)
|
||||
itemPrefix := pathUtils.Base(sourceDir)
|
||||
if itemPrefix == "/" {
|
||||
itemPrefix = ""
|
||||
}
|
||||
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -printf '%s' | sed 's|^|%s/|') -zcf %s %s %s", sourceDir, "%P\n", itemPrefix, targetDir+"/"+name, excludeRules, path)
|
||||
global.LOG.Debug(commands)
|
||||
}
|
||||
stdout, err := cmd.ExecWithTimeOut(commands, 24*time.Hour)
|
||||
@ -223,19 +226,19 @@ func (u *CronjobService) handleCutWebsiteLog(cronjob *model.Cronjob, startTime t
|
||||
if err != nil {
|
||||
return msgs, "", nil
|
||||
}
|
||||
baseDir := path.Join(nginx.GetPath(), "www", "sites")
|
||||
baseDir := pathUtils.Join(nginx.GetPath(), "www", "sites")
|
||||
fileOp := files.NewFileOp()
|
||||
for _, website := range websites {
|
||||
websiteLogDir := path.Join(baseDir, website.Alias, "log")
|
||||
srcAccessLogPath := path.Join(websiteLogDir, "access.log")
|
||||
srcErrorLogPath := path.Join(websiteLogDir, "error.log")
|
||||
dstLogDir := path.Join(global.CONF.System.Backup, "log", "website", website.Alias)
|
||||
websiteLogDir := pathUtils.Join(baseDir, website.Alias, "log")
|
||||
srcAccessLogPath := pathUtils.Join(websiteLogDir, "access.log")
|
||||
srcErrorLogPath := pathUtils.Join(websiteLogDir, "error.log")
|
||||
dstLogDir := pathUtils.Join(global.CONF.System.Backup, "log", "website", website.Alias)
|
||||
if !fileOp.Stat(dstLogDir) {
|
||||
_ = os.MkdirAll(dstLogDir, 0755)
|
||||
}
|
||||
|
||||
dstName := fmt.Sprintf("%s_log_%s.gz", website.PrimaryDomain, startTime.Format(constant.DateTimeSlimLayout))
|
||||
dstFilePath := path.Join(dstLogDir, dstName)
|
||||
dstFilePath := pathUtils.Join(dstLogDir, dstName)
|
||||
filePaths = append(filePaths, dstFilePath)
|
||||
|
||||
if err = backupLogFile(dstFilePath, websiteLogDir, fileOp); err != nil {
|
||||
@ -249,7 +252,6 @@ func (u *CronjobService) handleCutWebsiteLog(cronjob *model.Cronjob, startTime t
|
||||
_ = fileOp.WriteFile(srcErrorLogPath, strings.NewReader(""), 0755)
|
||||
}
|
||||
msg := i18n.GetMsgWithMap("CutWebsiteLogSuccess", map[string]interface{}{"name": website.PrimaryDomain, "path": dstFilePath})
|
||||
global.LOG.Infof(msg)
|
||||
msgs = append(msgs, msg)
|
||||
}
|
||||
u.removeExpiredLog(*cronjob)
|
||||
@ -258,18 +260,18 @@ func (u *CronjobService) handleCutWebsiteLog(cronjob *model.Cronjob, startTime t
|
||||
|
||||
func backupLogFile(dstFilePath, websiteLogDir string, fileOp files.FileOp) error {
|
||||
if err := cmd.ExecCmd(fmt.Sprintf("tar -czf %s -C %s %s", dstFilePath, websiteLogDir, strings.Join([]string{"access.log", "error.log"}, " "))); err != nil {
|
||||
dstDir := path.Dir(dstFilePath)
|
||||
if err = fileOp.Copy(path.Join(websiteLogDir, "access.log"), dstDir); err != nil {
|
||||
dstDir := pathUtils.Dir(dstFilePath)
|
||||
if err = fileOp.Copy(pathUtils.Join(websiteLogDir, "access.log"), dstDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = fileOp.Copy(path.Join(websiteLogDir, "error.log"), dstDir); err != nil {
|
||||
if err = fileOp.Copy(pathUtils.Join(websiteLogDir, "error.log"), dstDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = cmd.ExecCmd(fmt.Sprintf("tar -czf %s -C %s %s", dstFilePath, dstDir, strings.Join([]string{"access.log", "error.log"}, " "))); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = fileOp.DeleteFile(path.Join(dstDir, "access.log"))
|
||||
_ = fileOp.DeleteFile(path.Join(dstDir, "error.log"))
|
||||
_ = fileOp.DeleteFile(pathUtils.Join(dstDir, "access.log"))
|
||||
_ = fileOp.DeleteFile(pathUtils.Join(dstDir, "error.log"))
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
@ -287,8 +289,8 @@ func (u *CronjobService) uploadCronjobBackFile(cronjob model.Cronjob, accountMap
|
||||
cloudSrc := strings.TrimPrefix(file, global.CONF.System.TmpDir+"/")
|
||||
for _, account := range accounts {
|
||||
if len(account) != 0 {
|
||||
global.LOG.Debugf("start upload file to %s, dir: %s", accountMap[account].name, path.Join(accountMap[account].backupPath, cloudSrc))
|
||||
if _, err := accountMap[account].client.Upload(file, path.Join(accountMap[account].backupPath, cloudSrc)); err != nil {
|
||||
global.LOG.Debugf("start upload file to %s, dir: %s", accountMap[account].name, pathUtils.Join(accountMap[account].backupPath, cloudSrc))
|
||||
if _, err := accountMap[account].client.Upload(file, pathUtils.Join(accountMap[account].backupPath, cloudSrc)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
global.LOG.Debugf("upload successful!")
|
||||
@ -298,7 +300,6 @@ func (u *CronjobService) uploadCronjobBackFile(cronjob model.Cronjob, accountMap
|
||||
}
|
||||
|
||||
func (u *CronjobService) removeExpiredBackup(cronjob model.Cronjob, accountMap map[string]backupClientHelper, record model.BackupRecord) {
|
||||
global.LOG.Infof("start to handle remove expired, retain copies: %d", cronjob.RetainCopies)
|
||||
var opts []repo.DBOption
|
||||
opts = append(opts, commonRepo.WithByFrom("cronjob"))
|
||||
opts = append(opts, backupRepo.WithByCronID(cronjob.ID))
|
||||
@ -317,14 +318,14 @@ func (u *CronjobService) removeExpiredBackup(cronjob model.Cronjob, accountMap m
|
||||
if cronjob.Type == "snapshot" {
|
||||
for _, account := range accounts {
|
||||
if len(account) != 0 {
|
||||
_, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, "system_snapshot", records[i].FileName))
|
||||
_, _ = accountMap[account].client.Delete(pathUtils.Join(accountMap[account].backupPath, "system_snapshot", records[i].FileName))
|
||||
}
|
||||
}
|
||||
_ = snapshotRepo.Delete(commonRepo.WithByName(strings.TrimSuffix(records[i].FileName, ".tar.gz")))
|
||||
} else {
|
||||
for _, account := range accounts {
|
||||
if len(account) != 0 {
|
||||
_, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, records[i].FileDir, records[i].FileName))
|
||||
_, _ = accountMap[account].client.Delete(pathUtils.Join(accountMap[account].backupPath, records[i].FileDir, records[i].FileName))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -333,7 +334,6 @@ func (u *CronjobService) removeExpiredBackup(cronjob model.Cronjob, accountMap m
|
||||
}
|
||||
|
||||
func (u *CronjobService) removeExpiredLog(cronjob model.Cronjob) {
|
||||
global.LOG.Infof("start to handle remove expired, retain copies: %d", cronjob.RetainCopies)
|
||||
records, _ := cronjobRepo.ListRecord(cronjobRepo.WithByJobID(int(cronjob.ID)), commonRepo.WithOrderBy("created_at desc"))
|
||||
if len(records) <= int(cronjob.RetainCopies) {
|
||||
return
|
||||
|
@ -176,12 +176,12 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
|
||||
currentInfo.Procs = hostInfo.Procs
|
||||
|
||||
currentInfo.CPUTotal, _ = cpu.Counts(true)
|
||||
totalPercent, _ := cpu.Percent(0, false)
|
||||
totalPercent, _ := cpu.Percent(100*time.Millisecond, false)
|
||||
if len(totalPercent) == 1 {
|
||||
currentInfo.CPUUsedPercent = totalPercent[0]
|
||||
currentInfo.CPUUsed = currentInfo.CPUUsedPercent * 0.01 * float64(currentInfo.CPUTotal)
|
||||
}
|
||||
currentInfo.CPUPercent, _ = cpu.Percent(0, true)
|
||||
currentInfo.CPUPercent, _ = cpu.Percent(100*time.Millisecond, true)
|
||||
|
||||
loadInfo, _ := load.Avg()
|
||||
currentInfo.Load1 = loadInfo.Load1
|
||||
@ -375,13 +375,13 @@ func loadDiskInfo() []dto.DiskInfo {
|
||||
if strings.HasPrefix(fields[6], "/snap") || len(strings.Split(fields[6], "/")) > 10 {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(fields[1]) == "tmpfs" {
|
||||
if strings.TrimSpace(fields[1]) == "tmpfs" || strings.TrimSpace(fields[1]) == "overlay" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(fields[2], "K") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(fields[6], "docker") {
|
||||
if strings.Contains(fields[6], "docker") || strings.Contains(fields[6], "podman") || strings.Contains(fields[6], "containerd") || strings.HasPrefix(fields[6], "/var/lib/containers") {
|
||||
continue
|
||||
}
|
||||
isExclude := false
|
||||
|
@ -141,6 +141,10 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
|
||||
}
|
||||
|
||||
func (u *MysqlService) BindUser(req dto.BindUser) error {
|
||||
if cmd.CheckIllegal(req.Username, req.Password, req.Permission) {
|
||||
return buserr.New(constant.ErrCmdIllegal)
|
||||
}
|
||||
|
||||
dbItem, err := mysqlRepo.Get(mysqlRepo.WithByMysqlName(req.Database), commonRepo.WithByName(req.DB))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -4,14 +4,17 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/systemctl"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -198,6 +201,9 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate) error {
|
||||
}
|
||||
if len(daemonMap) == 0 {
|
||||
_ = os.Remove(constant.DaemonJsonPath)
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
newJson, err := json.MarshalIndent(daemonMap, "", "\t")
|
||||
@ -207,10 +213,12 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate) error {
|
||||
if err := os.WriteFile(constant.DaemonJsonPath, newJson, 0640); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdout, err := cmd.Exec("systemctl restart docker")
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -244,6 +252,7 @@ func (u *DockerService) UpdateLogOption(req dto.LogOption) error {
|
||||
changeLogOption(daemonMap, req.LogMaxFile, req.LogMaxSize)
|
||||
if len(daemonMap) == 0 {
|
||||
_ = os.Remove(constant.DaemonJsonPath)
|
||||
_ = restartDocker()
|
||||
return nil
|
||||
}
|
||||
newJson, err := json.MarshalIndent(daemonMap, "", "\t")
|
||||
@ -254,9 +263,12 @@ func (u *DockerService) UpdateLogOption(req dto.LogOption) error {
|
||||
return err
|
||||
}
|
||||
|
||||
stdout, err := cmd.Exec("systemctl restart docker")
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -284,6 +296,7 @@ func (u *DockerService) UpdateIpv6Option(req dto.Ipv6Option) error {
|
||||
}
|
||||
if len(daemonMap) == 0 {
|
||||
_ = os.Remove(constant.DaemonJsonPath)
|
||||
_ = restartDocker()
|
||||
return nil
|
||||
}
|
||||
newJson, err := json.MarshalIndent(daemonMap, "", "\t")
|
||||
@ -294,9 +307,12 @@ func (u *DockerService) UpdateIpv6Option(req dto.Ipv6Option) error {
|
||||
return err
|
||||
}
|
||||
|
||||
stdout, err := cmd.Exec("systemctl restart docker")
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -304,6 +320,9 @@ func (u *DockerService) UpdateIpv6Option(req dto.Ipv6Option) error {
|
||||
func (u *DockerService) UpdateConfByFile(req dto.DaemonJsonUpdateByFile) error {
|
||||
if len(req.File) == 0 {
|
||||
_ = os.Remove(constant.DaemonJsonPath)
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err := createIfNotExistDaemonJsonFile()
|
||||
@ -319,19 +338,38 @@ func (u *DockerService) UpdateConfByFile(req dto.DaemonJsonUpdateByFile) error {
|
||||
_, _ = write.WriteString(req.File)
|
||||
write.Flush()
|
||||
|
||||
stdout, err := cmd.Exec("systemctl restart docker")
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
|
||||
service := "docker"
|
||||
if req.Operation == "stop" {
|
||||
service = "docker.socket"
|
||||
sudo := cmd.SudoHandleCmd()
|
||||
dockerCmd, err := getDockerRestartCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdout, err := cmd.Execf("systemctl %s %s ", req.Operation, service)
|
||||
if req.Operation == "stop" {
|
||||
isSocketActive, _ := systemctl.IsActive("docker.socket")
|
||||
if isSocketActive {
|
||||
std, err := cmd.Execf("%s systemctl stop docker.socket", sudo)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("handle systemctl stop docker.socket failed, err: %v", std)
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.Operation == "restart" {
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
stdout, err := cmd.Execf("%s %s %s ", dockerCmd, req.Operation, service)
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
}
|
||||
@ -386,3 +424,41 @@ func changeLogOption(daemonMap map[string]interface{}, logMaxFile, logMaxSize st
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateDockerConfig() error {
|
||||
if !cmd.Which("dockerd") {
|
||||
return nil
|
||||
}
|
||||
stdout, err := cmd.Exec("dockerd --validate")
|
||||
if strings.Contains(stdout, "unknown flag: --validate") {
|
||||
return nil
|
||||
}
|
||||
if err != nil || (stdout != "" && strings.TrimSpace(stdout) != "configuration OK") {
|
||||
return fmt.Errorf("Docker configuration validation failed, err: %v", stdout)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDockerRestartCommand() (string, error) {
|
||||
stdout, err := cmd.Exec("which docker")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to find docker: %v", err)
|
||||
}
|
||||
dockerPath := stdout
|
||||
if strings.Contains(dockerPath, "snap") {
|
||||
return "snap", nil
|
||||
}
|
||||
return "systemctl", nil
|
||||
}
|
||||
|
||||
func restartDocker() error {
|
||||
restartCmd, err := getDockerRestartCommand()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdout, err := cmd.Execf("%s restart docker", restartCmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to restart Docker: %s", stdout)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -588,14 +588,6 @@ func (u *FirewallService) addPortsBeforeStart(client firewall.FirewallClient) er
|
||||
if err := client.Port(fireClient.FireInfo{Port: "443", Protocol: "tcp", Strategy: "accept"}, "add"); err != nil {
|
||||
return err
|
||||
}
|
||||
apps := u.loadPortByApp()
|
||||
for _, app := range apps {
|
||||
if len(app.HttpPort) != 0 && app.HttpPort != "0" {
|
||||
if err := client.Port(fireClient.FireInfo{Port: app.HttpPort, Protocol: "tcp", Strategy: "accept"}, "add"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return client.Reload()
|
||||
}
|
||||
|
@ -84,8 +84,11 @@ func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
|
||||
if imageRepo.ID != 0 {
|
||||
return constant.ErrRecordExist
|
||||
}
|
||||
|
||||
if req.Protocol == "http" {
|
||||
_ = u.handleRegistries(req.DownloadUrl, "", "create")
|
||||
if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil {
|
||||
return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err)
|
||||
}
|
||||
stdout, err := cmd.Exec("systemctl restart docker")
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
@ -113,22 +116,18 @@ func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
|
||||
}
|
||||
}
|
||||
|
||||
if req.Auth {
|
||||
if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := copier.Copy(&imageRepo, &req); err != nil {
|
||||
return errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||
}
|
||||
|
||||
imageRepo.Status = constant.StatusSuccess
|
||||
if req.Auth {
|
||||
if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil {
|
||||
imageRepo.Status = constant.StatusFailed
|
||||
imageRepo.Message = err.Error()
|
||||
}
|
||||
}
|
||||
if err := imageRepoRepo.Create(&imageRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return imageRepoRepo.Create(&imageRepo)
|
||||
}
|
||||
|
||||
func (u *ImageRepoService) BatchDelete(req dto.ImageRepoDelete) error {
|
||||
@ -154,32 +153,47 @@ func (u *ImageRepoService) Update(req dto.ImageRepoUpdate) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if repo.DownloadUrl != req.DownloadUrl || (!repo.Auth && req.Auth) {
|
||||
_ = u.handleRegistries(req.DownloadUrl, repo.DownloadUrl, "update")
|
||||
if repo.Protocol == "http" && req.Protocol == "https" {
|
||||
if err := u.handleRegistries("", repo.DownloadUrl, "delete"); err != nil {
|
||||
return fmt.Errorf("delete registry %s failed, err: %v", repo.DownloadUrl, err)
|
||||
}
|
||||
}
|
||||
if repo.Protocol == "http" && req.Protocol == "http" {
|
||||
if err := u.handleRegistries(req.DownloadUrl, repo.DownloadUrl, "update"); err != nil {
|
||||
return fmt.Errorf("update registry %s => %s failed, err: %v", repo.DownloadUrl, req.DownloadUrl, err)
|
||||
}
|
||||
}
|
||||
if repo.Protocol == "https" && req.Protocol == "http" {
|
||||
if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil {
|
||||
return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err)
|
||||
}
|
||||
}
|
||||
if repo.Auth != req.Auth || repo.DownloadUrl != req.DownloadUrl {
|
||||
if repo.Auth {
|
||||
_, _ = cmd.ExecWithCheck("docker", "logout", repo.DownloadUrl)
|
||||
}
|
||||
stdout, err := cmd.Exec("systemctl restart docker")
|
||||
if err != nil {
|
||||
return errors.New(string(stdout))
|
||||
if req.Auth {
|
||||
if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateDockerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := restartDocker(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
upMap := make(map[string]interface{})
|
||||
upMap["download_url"] = req.DownloadUrl
|
||||
upMap["protocol"] = req.Protocol
|
||||
upMap["username"] = req.Username
|
||||
upMap["password"] = req.Password
|
||||
upMap["auth"] = req.Auth
|
||||
|
||||
upMap["status"] = constant.StatusSuccess
|
||||
upMap["message"] = ""
|
||||
if req.Auth {
|
||||
if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil {
|
||||
upMap["status"] = constant.StatusFailed
|
||||
upMap["message"] = err.Error()
|
||||
}
|
||||
}
|
||||
return imageRepoRepo.Update(req.ID, upMap)
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@ func NewIMonitorService() IMonitorService {
|
||||
}
|
||||
|
||||
func (m *MonitorService) LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorData, error) {
|
||||
loc, _ := time.LoadLocation(common.LoadTimeZone())
|
||||
loc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
req.StartTime = req.StartTime.In(loc)
|
||||
req.EndTime = req.EndTime.In(loc)
|
||||
|
||||
|
@ -27,7 +27,7 @@ type SnapshotService struct {
|
||||
}
|
||||
|
||||
type ISnapshotService interface {
|
||||
SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
SearchWithPage(req dto.PageSnapshot) (int64, interface{}, error)
|
||||
LoadSize(req dto.SearchWithPage) ([]dto.SnapshotFile, error)
|
||||
LoadSnapshotData() (dto.SnapshotData, error)
|
||||
SnapshotCreate(req dto.SnapshotCreate) error
|
||||
@ -46,8 +46,8 @@ func NewISnapshotService() ISnapshotService {
|
||||
return &SnapshotService{}
|
||||
}
|
||||
|
||||
func (u *SnapshotService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) {
|
||||
total, records, err := snapshotRepo.Page(req.Page, req.PageSize, commonRepo.WithByLikeName(req.Info))
|
||||
func (u *SnapshotService) SearchWithPage(req dto.PageSnapshot) (int64, interface{}, error) {
|
||||
total, records, err := snapshotRepo.Page(req.Page, req.PageSize, commonRepo.WithByLikeName(req.Info), commonRepo.WithOrderRuleBy(req.OrderBy, req.Order))
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
@ -306,16 +306,7 @@ func recoverAppData(src string, itemHelper *snapRecoverHelper) error {
|
||||
go func(app model.AppInstall) {
|
||||
defer wg.Done()
|
||||
dockerComposePath := app.GetComposePath()
|
||||
out, err := compose.Down(dockerComposePath)
|
||||
if err != nil {
|
||||
_ = handleErr(app, err, out)
|
||||
return
|
||||
}
|
||||
out, err = compose.Up(dockerComposePath)
|
||||
if err != nil {
|
||||
_ = handleErr(app, err, out)
|
||||
return
|
||||
}
|
||||
_, _ = compose.Up(dockerComposePath)
|
||||
app.Status = constant.Running
|
||||
_ = appInstallRepo.Save(context.Background(), &app)
|
||||
}(appInstalls[i])
|
||||
|
@ -124,6 +124,16 @@ func (u *SSHService) OperateSSH(operation string) error {
|
||||
if operation == "enable" || operation == "disable" {
|
||||
serviceName += ".service"
|
||||
}
|
||||
if operation == "stop" {
|
||||
isSocketActive, _ := systemctl.IsActive(serviceName + ".socket")
|
||||
if isSocketActive {
|
||||
std, err := cmd.Execf("%s systemctl stop %s", sudo, serviceName+".socket")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("handle systemctl stop %s.socket failed, err: %v", serviceName, std)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stdout, err := cmd.Execf("%s systemctl %s %s", sudo, operation, serviceName)
|
||||
if err != nil {
|
||||
if strings.Contains(stdout, "alias name or linked unit file") {
|
||||
@ -308,7 +318,7 @@ func (u *SSHService) LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error) {
|
||||
|
||||
showCountFrom := (req.Page - 1) * req.PageSize
|
||||
showCountTo := req.Page * req.PageSize
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZone())
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
qqWry, err := qqwry.NewQQwry()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load qqwry datas failed: %s", err)
|
||||
|
@ -128,7 +128,8 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFirewall = "ErrFirewall"
|
||||
ErrFirewallNone = "ErrFirewallNone"
|
||||
ErrFirewallBoth = "ErrFirewallBoth"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -17,14 +17,14 @@ import (
|
||||
)
|
||||
|
||||
func Run() {
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZone())
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
global.Cron = cron.New(cron.WithLocation(nyc), cron.WithChain(cron.Recover(cron.DefaultLogger)), cron.WithChain(cron.DelayIfStillRunning(cron.DefaultLogger)))
|
||||
|
||||
var (
|
||||
interval model.Setting
|
||||
status model.Setting
|
||||
)
|
||||
syncBeforeStart()
|
||||
go syncBeforeStart()
|
||||
if err := global.DB.Where("key = ?", "MonitorStatus").Find(&status).Error; err != nil {
|
||||
global.LOG.Errorf("load monitor status from db failed, err: %v", err)
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ func (ssl *ssl) Run() {
|
||||
sslRepo := repo.NewISSLRepo()
|
||||
sslService := service.NewIWebsiteSSLService()
|
||||
sslList, _ := sslRepo.List()
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZone())
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
global.LOG.Info("The scheduled certificate update task is currently in progress ...")
|
||||
now := time.Now().Add(10 * time.Second)
|
||||
for _, s := range sslList {
|
||||
|
@ -20,7 +20,7 @@ func NewWebsiteJob() *website {
|
||||
}
|
||||
|
||||
func (w *website) Run() {
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZone())
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
websites, _ := repo.NewIWebsiteRepo().List()
|
||||
global.LOG.Info("Website scheduled task in progress ...")
|
||||
now := time.Now().Add(10 * time.Minute)
|
||||
|
@ -181,7 +181,8 @@ ErrConfigAlreadyExist: "A configuration file with the same name already exists"
|
||||
ErrUserFindErr: "Failed to find user {{ .name }} {{ .err }}"
|
||||
|
||||
#ssh
|
||||
ErrFirewall: "No firewalld or ufw service is detected. Please check and try again!"
|
||||
ErrFirewallNone: "No firewalld or ufw service detected on the system. Please check and try again!"
|
||||
ErrFirewallBoth: "Both firewalld and ufw services are detected on the system. To avoid conflicts, please uninstall one and try again!"
|
||||
|
||||
#cronjob
|
||||
ErrCutWebsiteLog: "{{ .name }} website log cutting failed, error {{ .err }}"
|
||||
|
@ -182,7 +182,8 @@ ErrConfigAlreadyExist: "已存在同名配置文件"
|
||||
ErrUserFindErr: "用戶 {{ .name }} 查找失敗 {{ .err }}"
|
||||
|
||||
#ssh
|
||||
ErrFirewall: "當前未檢測到系統 firewalld 或 ufw 服務,請檢查後重試!"
|
||||
ErrFirewallNone: "未檢測到系統 firewalld 或 ufw 服務,請檢查後重試!"
|
||||
ErrFirewallBoth: "檢測到系統同時存在 firewalld 或 ufw 服務,為避免衝突,請卸載後重試!"
|
||||
|
||||
#cronjob
|
||||
ErrCutWebsiteLog: "{{ .name }} 網站日誌切割失敗,錯誤 {{ .err }}"
|
||||
|
@ -180,7 +180,8 @@ ErrConfigAlreadyExist: "已存在同名配置文件"
|
||||
ErrUserFindErr: "用户 {{ .name }} 查找失败 {{ .err }}"
|
||||
|
||||
#ssh
|
||||
ErrFirewall: "当前未检测到系统 firewalld 或 ufw 服务,请检查后重试!"
|
||||
ErrFirewallNone: "未检测到系统 firewalld 或 ufw 服务,请检查后重试!"
|
||||
ErrFirewallBoth: "检测到系统同时存在 firewalld 或 ufw 服务,为避免冲突,请卸载后重试!"
|
||||
|
||||
#cronjob
|
||||
ErrCutWebsiteLog: "{{ .name }} 网站日志切割失败,错误 {{ .err }}"
|
||||
|
@ -27,12 +27,15 @@ func NewS3Client(vars map[string]interface{}) (*s3Client, error) {
|
||||
if len(scType) == 0 {
|
||||
scType = "Standard"
|
||||
}
|
||||
mode := loadParamFromVars("mode", vars)
|
||||
if len(mode) == 0 {
|
||||
mode = "virtual hosted"
|
||||
}
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
|
||||
Endpoint: aws.String(endpoint),
|
||||
Region: aws.String(region),
|
||||
DisableSSL: aws.Bool(true),
|
||||
S3ForcePathStyle: aws.Bool(false),
|
||||
Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
|
||||
Endpoint: aws.String(endpoint),
|
||||
Region: aws.String(region),
|
||||
DisableSSL: aws.Bool(true), S3ForcePathStyle: aws.Bool(mode == "path"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -238,7 +238,7 @@ func SudoHandleCmd() string {
|
||||
|
||||
func Which(name string) bool {
|
||||
stdout, err := Execf("which %s", name)
|
||||
if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0 && strings.HasPrefix(stdout, "/")) {
|
||||
if err != nil || (len(strings.ReplaceAll(stdout, "\n", "")) == 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
@ -268,13 +268,6 @@ func LoadSizeUnit2F(value float64) string {
|
||||
return fmt.Sprintf("%.2f", value)
|
||||
}
|
||||
|
||||
func LoadTimeZone() string {
|
||||
loc := time.Now().Location()
|
||||
if _, err := time.LoadLocation(loc.String()); err != nil {
|
||||
return "Asia/Shanghai"
|
||||
}
|
||||
return loc.String()
|
||||
}
|
||||
func LoadTimeZoneByCmd() string {
|
||||
loc := time.Now().Location().String()
|
||||
if _, err := time.LoadLocation(loc); err != nil {
|
||||
|
@ -729,6 +729,7 @@ func (f FileOp) TarGzCompressPro(withDir bool, src, dst, secret, exclusionRules
|
||||
exStr := ""
|
||||
excludes := strings.Split(exclusionRules, ";")
|
||||
excludes = append(excludes, "*.sock")
|
||||
excludes = append(excludes, "*.socket")
|
||||
for _, exclude := range excludes {
|
||||
if len(exclude) == 0 {
|
||||
continue
|
||||
|
@ -1,10 +1,9 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client"
|
||||
)
|
||||
|
||||
@ -29,11 +28,18 @@ type FirewallClient interface {
|
||||
}
|
||||
|
||||
func NewFirewallClient() (FirewallClient, error) {
|
||||
if _, err := os.Stat("/usr/sbin/firewalld"); err == nil {
|
||||
firewalld := cmd.Which("firewalld")
|
||||
ufw := cmd.Which("ufw")
|
||||
|
||||
if firewalld && ufw {
|
||||
return nil, buserr.New(constant.ErrFirewallBoth)
|
||||
}
|
||||
|
||||
if firewalld {
|
||||
return client.NewFirewalld()
|
||||
}
|
||||
if _, err := os.Stat("/usr/sbin/ufw"); err == nil {
|
||||
if ufw {
|
||||
return client.NewUfw()
|
||||
}
|
||||
return nil, buserr.New(constant.ErrFirewall)
|
||||
return nil, buserr.New(constant.ErrFirewallNone)
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
)
|
||||
|
||||
@ -118,6 +119,9 @@ func (f *Firewall) ListPort() ([]FireInfo, error) {
|
||||
}
|
||||
|
||||
func (f *Firewall) ListForward() ([]FireInfo, error) {
|
||||
if err := f.EnableForward(); err != nil {
|
||||
global.LOG.Errorf("init port forward failed, err: %v", err)
|
||||
}
|
||||
stdout, err := cmd.Exec("firewall-cmd --zone=public --list-forward-ports")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
)
|
||||
|
||||
@ -108,6 +109,12 @@ func (f *Ufw) ListForward() ([]FireInfo, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
panelChian, _ := cmd.Execf("%s iptables -t nat -L -n | grep 'Chain 1PANEL'", iptables.CmdStr)
|
||||
if len(strings.ReplaceAll(panelChian, "\n", "")) == 0 {
|
||||
if err := f.EnableForward(); err != nil {
|
||||
global.LOG.Errorf("init port forward failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
rules, err := iptables.NatList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -1,303 +0,0 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func init() {}
|
||||
|
||||
type dumpOption struct {
|
||||
isData bool
|
||||
|
||||
tables []string
|
||||
isAllTable bool
|
||||
isDropTable bool
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
type DumpOption func(*dumpOption)
|
||||
|
||||
func WithDropTable() DumpOption {
|
||||
return func(option *dumpOption) {
|
||||
option.isDropTable = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithData() DumpOption {
|
||||
return func(option *dumpOption) {
|
||||
option.isData = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithWriter(writer io.Writer) DumpOption {
|
||||
return func(option *dumpOption) {
|
||||
option.writer = writer
|
||||
}
|
||||
}
|
||||
|
||||
func Dump(dns string, opts ...DumpOption) error {
|
||||
start := time.Now()
|
||||
global.LOG.Infof("dump start at %s\n", start.Format(constant.DateTimeLayout))
|
||||
defer func() {
|
||||
end := time.Now()
|
||||
global.LOG.Infof("dump end at %s, cost %s\n", end.Format(constant.DateTimeLayout), end.Sub(start))
|
||||
}()
|
||||
|
||||
var err error
|
||||
|
||||
var o dumpOption
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
}
|
||||
|
||||
if len(o.tables) == 0 {
|
||||
o.isAllTable = true
|
||||
}
|
||||
|
||||
if o.writer == nil {
|
||||
o.writer = os.Stdout
|
||||
}
|
||||
|
||||
buf := bufio.NewWriter(o.writer)
|
||||
defer buf.Flush()
|
||||
|
||||
itemFile, lineNumber := "", 0
|
||||
|
||||
itemFile += "-- ----------------------------\n"
|
||||
itemFile += "-- MySQL Database Dump\n"
|
||||
itemFile += "-- Start Time: " + start.Format(constant.DateTimeLayout) + "\n"
|
||||
itemFile += "-- ----------------------------\n\n\n"
|
||||
|
||||
db, err := sql.Open("mysql", dns)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("open mysql db failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
dbName, err := getDBNameFromDNS(dns)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get db name from dns failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Exec(fmt.Sprintf("USE `%s`", dbName))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("exec `use %s` failed, err: %v", dbName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
var tables []string
|
||||
if o.isAllTable {
|
||||
tmp, err := getAllTables(db)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get all tables failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
tables = tmp
|
||||
} else {
|
||||
tables = o.tables
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
if o.isDropTable {
|
||||
itemFile += fmt.Sprintf("DROP TABLE IF EXISTS `%s`;\n", table)
|
||||
}
|
||||
|
||||
itemFile += "-- ----------------------------\n"
|
||||
itemFile += fmt.Sprintf("-- Table structure for %s\n", table)
|
||||
itemFile += "-- ----------------------------\n"
|
||||
|
||||
createTableSQL, err := getCreateTableSQL(db, table)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get create table sql failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
itemFile += createTableSQL
|
||||
itemFile += ";\n\n\n\n"
|
||||
|
||||
if o.isData {
|
||||
itemFile += "-- ----------------------------\n"
|
||||
itemFile += fmt.Sprintf("-- Records of %s\n", table)
|
||||
itemFile += "-- ----------------------------\n"
|
||||
|
||||
lineRows, err := db.Query(fmt.Sprintf("SELECT * FROM `%s`", table))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("exec `select * from %s` failed, err: %v", table, err)
|
||||
return err
|
||||
}
|
||||
defer lineRows.Close()
|
||||
|
||||
var columns []string
|
||||
columns, err = lineRows.Columns()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get columes failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
columnTypes, err := lineRows.ColumnTypes()
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get colume types failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
for lineRows.Next() {
|
||||
row := make([]interface{}, len(columns))
|
||||
rowPointers := make([]interface{}, len(columns))
|
||||
for i := range columns {
|
||||
rowPointers[i] = &row[i]
|
||||
}
|
||||
if err = lineRows.Scan(rowPointers...); err != nil {
|
||||
global.LOG.Errorf("scan row data failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
ssql := loadDataSql(row, columnTypes, table)
|
||||
if len(ssql) != 0 {
|
||||
itemFile += ssql
|
||||
lineNumber++
|
||||
}
|
||||
if lineNumber > 500 {
|
||||
_, _ = buf.WriteString(itemFile)
|
||||
itemFile = ""
|
||||
lineNumber = 0
|
||||
}
|
||||
}
|
||||
|
||||
itemFile += "\n\n"
|
||||
}
|
||||
}
|
||||
|
||||
itemFile += "-- ----------------------------\n"
|
||||
itemFile += "-- Dumped by mysqldump\n"
|
||||
itemFile += "-- Cost Time: " + time.Since(start).String() + "\n"
|
||||
itemFile += "-- ----------------------------\n"
|
||||
|
||||
_, _ = buf.WriteString(itemFile)
|
||||
_ = buf.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCreateTableSQL(db *sql.DB, table string) (string, error) {
|
||||
var createTableSQL string
|
||||
err := db.QueryRow(fmt.Sprintf("SHOW CREATE TABLE `%s`", table)).Scan(&table, &createTableSQL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
createTableSQL = strings.Replace(createTableSQL, "CREATE TABLE", "CREATE TABLE IF NOT EXISTS", 1)
|
||||
return createTableSQL, nil
|
||||
}
|
||||
|
||||
func getAllTables(db *sql.DB) ([]string, error) {
|
||||
var tables []string
|
||||
rows, err := db.Query("SHOW TABLES")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var table string
|
||||
err = rows.Scan(&table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tables = append(tables, table)
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func loadDataSql(row []interface{}, columnTypes []*sql.ColumnType, table string) string {
|
||||
ssql := "INSERT INTO `" + table + "` VALUES ("
|
||||
for i, col := range row {
|
||||
if col == nil {
|
||||
ssql += "NULL"
|
||||
} else {
|
||||
Type := columnTypes[i].DatabaseTypeName()
|
||||
Type = strings.Replace(Type, "UNSIGNED", "", -1)
|
||||
Type = strings.Replace(Type, " ", "", -1)
|
||||
switch Type {
|
||||
case "TINYINT", "SMALLINT", "MEDIUMINT", "INT", "INTEGER", "BIGINT":
|
||||
if bs, ok := col.([]byte); ok {
|
||||
ssql += string(bs)
|
||||
} else {
|
||||
ssql += fmt.Sprintf("%d", col)
|
||||
}
|
||||
case "FLOAT", "DOUBLE":
|
||||
if bs, ok := col.([]byte); ok {
|
||||
ssql += string(bs)
|
||||
} else {
|
||||
ssql += fmt.Sprintf("%f", col)
|
||||
}
|
||||
case "DECIMAL", "DEC":
|
||||
ssql += fmt.Sprintf("%s", col)
|
||||
|
||||
case "DATE":
|
||||
t, ok := col.(time.Time)
|
||||
if !ok {
|
||||
global.LOG.Errorf("the DATE type conversion failed, err value: %v", col)
|
||||
return ""
|
||||
}
|
||||
ssql += fmt.Sprintf("'%s'", t.Format("2006-01-02"))
|
||||
case "DATETIME":
|
||||
t, ok := col.(time.Time)
|
||||
if !ok {
|
||||
global.LOG.Errorf("the DATETIME type conversion failed, err value: %v", col)
|
||||
return ""
|
||||
}
|
||||
ssql += fmt.Sprintf("'%s'", t.Format(constant.DateTimeLayout))
|
||||
case "TIMESTAMP":
|
||||
t, ok := col.(time.Time)
|
||||
if !ok {
|
||||
global.LOG.Errorf("the TIMESTAMP type conversion failed, err value: %v", col)
|
||||
return ""
|
||||
}
|
||||
ssql += fmt.Sprintf("'%s'", t.Format(constant.DateTimeLayout))
|
||||
case "TIME":
|
||||
t, ok := col.([]byte)
|
||||
if !ok {
|
||||
global.LOG.Errorf("the TIME type conversion failed, err value: %v", col)
|
||||
return ""
|
||||
}
|
||||
ssql += fmt.Sprintf("'%s'", string(t))
|
||||
case "YEAR":
|
||||
t, ok := col.([]byte)
|
||||
if !ok {
|
||||
global.LOG.Errorf("the YEAR type conversion failed, err value: %v", col)
|
||||
return ""
|
||||
}
|
||||
ssql += string(t)
|
||||
case "CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT":
|
||||
ssql += fmt.Sprintf("'%s'", strings.Replace(fmt.Sprintf("%s", col), "'", "''", -1))
|
||||
case "BIT", "BINARY", "VARBINARY", "TINYBLOB", "BLOB", "MEDIUMBLOB", "LONGBLOB":
|
||||
ssql += fmt.Sprintf("0x%X", col)
|
||||
case "ENUM", "SET":
|
||||
ssql += fmt.Sprintf("'%s'", col)
|
||||
case "BOOL", "BOOLEAN":
|
||||
if col.(bool) {
|
||||
ssql += "true"
|
||||
} else {
|
||||
ssql += "false"
|
||||
}
|
||||
case "JSON":
|
||||
ssql += fmt.Sprintf("'%s'", col)
|
||||
default:
|
||||
global.LOG.Errorf("unsupported colume type: %s", Type)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
if i < len(row)-1 {
|
||||
ssql += ","
|
||||
}
|
||||
}
|
||||
ssql += ");\n"
|
||||
return ssql
|
||||
}
|
@ -1,244 +0,0 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
)
|
||||
|
||||
type sourceOption struct {
|
||||
dryRun bool
|
||||
mergeInsert int
|
||||
debug bool
|
||||
}
|
||||
type SourceOption func(*sourceOption)
|
||||
|
||||
func WithMergeInsert(size int) SourceOption {
|
||||
return func(o *sourceOption) {
|
||||
o.mergeInsert = size
|
||||
}
|
||||
}
|
||||
|
||||
type dbWrapper struct {
|
||||
DB *sql.DB
|
||||
debug bool
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
func newDBWrapper(db *sql.DB, dryRun, debug bool) *dbWrapper {
|
||||
|
||||
return &dbWrapper{
|
||||
DB: db,
|
||||
dryRun: dryRun,
|
||||
debug: debug,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *dbWrapper) Exec(query string, args ...interface{}) (sql.Result, error) {
|
||||
if db.debug {
|
||||
global.LOG.Debugf("query %s", query)
|
||||
}
|
||||
|
||||
if db.dryRun {
|
||||
return nil, nil
|
||||
}
|
||||
return db.DB.Exec(query, args...)
|
||||
}
|
||||
|
||||
func Source(dns string, reader io.Reader, opts ...SourceOption) error {
|
||||
start := time.Now()
|
||||
global.LOG.Infof("source start at %s", start.Format(constant.DateTimeLayout))
|
||||
defer func() {
|
||||
end := time.Now()
|
||||
global.LOG.Infof("source end at %s, cost %s", end.Format(constant.DateTimeLayout), end.Sub(start))
|
||||
}()
|
||||
|
||||
var err error
|
||||
var db *sql.DB
|
||||
var o sourceOption
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
}
|
||||
|
||||
dbName, err := getDBNameFromDNS(dns)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get db name from dns failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
db, err = sql.Open("mysql", dns)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("open mysql db failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
dbWrapper := newDBWrapper(db, o.dryRun, o.debug)
|
||||
|
||||
_, err = dbWrapper.Exec(fmt.Sprintf("USE `%s`;", dbName))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("exec `use %s` failed, err: %v", dbName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
db.SetConnMaxLifetime(3600)
|
||||
|
||||
r := bufio.NewReader(reader)
|
||||
_, err = dbWrapper.Exec("SET autocommit=0;")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("exec `set autocommit=0` failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
line, err := readLine(r)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
global.LOG.Errorf("read sql failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
ssql, err := trim(line)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("trim sql failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
afterInsertSql := ""
|
||||
if o.mergeInsert > 1 && strings.HasPrefix(ssql, "INSERT INTO") {
|
||||
var insertSQLs []string
|
||||
insertSQLs = append(insertSQLs, ssql)
|
||||
for i := 0; i < o.mergeInsert-1; i++ {
|
||||
line, err := readLine(r)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
ssql2, err := trim(line)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("trim merge insert sql failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
if strings.HasPrefix(ssql2, "INSERT INTO") {
|
||||
insertSQLs = append(insertSQLs, ssql2)
|
||||
continue
|
||||
}
|
||||
afterInsertSql = ssql2
|
||||
break
|
||||
}
|
||||
ssql, err = mergeInsert(insertSQLs)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("do merge insert failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = dbWrapper.Exec(ssql)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("exec sql failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
if len(afterInsertSql) != 0 {
|
||||
_, err = dbWrapper.Exec(afterInsertSql)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("exec sql failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = dbWrapper.Exec("COMMIT;")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("exec `commit` failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = dbWrapper.Exec("SET autocommit=1;")
|
||||
if err != nil {
|
||||
global.LOG.Errorf("exec `autocommit=1` failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeInsert(insertSQLs []string) (string, error) {
|
||||
if len(insertSQLs) == 0 {
|
||||
return "", errors.New("no input provided")
|
||||
}
|
||||
builder := strings.Builder{}
|
||||
sql1 := insertSQLs[0]
|
||||
sql1 = strings.TrimSuffix(sql1, ";")
|
||||
builder.WriteString(sql1)
|
||||
for i, insertSQL := range insertSQLs[1:] {
|
||||
if i < len(insertSQLs)-1 {
|
||||
builder.WriteString(",")
|
||||
}
|
||||
|
||||
valuesIdx := strings.Index(insertSQL, "VALUES")
|
||||
if valuesIdx == -1 {
|
||||
return "", errors.New("invalid SQL: missing VALUES keyword")
|
||||
}
|
||||
sqln := insertSQL[valuesIdx:]
|
||||
sqln = strings.TrimPrefix(sqln, "VALUES")
|
||||
sqln = strings.TrimSuffix(sqln, ";")
|
||||
builder.WriteString(sqln)
|
||||
|
||||
}
|
||||
builder.WriteString(";")
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func trim(s string) (string, error) {
|
||||
s = strings.TrimLeft(s, "\n")
|
||||
s = strings.TrimSpace(s)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func getDBNameFromDNS(dns string) (string, error) {
|
||||
ss1 := strings.Split(dns, "/")
|
||||
if len(ss1) == 2 {
|
||||
ss2 := strings.Split(ss1[1], "?")
|
||||
if len(ss2) == 2 {
|
||||
return ss2[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("dns error: %s", dns)
|
||||
}
|
||||
|
||||
func readLine(r *bufio.Reader) (string, error) {
|
||||
lineItem, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return lineItem, err
|
||||
}
|
||||
global.LOG.Errorf("read merge insert sql failed, err: %v", err)
|
||||
return "", err
|
||||
}
|
||||
if strings.HasSuffix(lineItem, ";\n") {
|
||||
return lineItem, nil
|
||||
}
|
||||
lineAppend, err := readLine(r)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return lineItem, err
|
||||
}
|
||||
global.LOG.Errorf("read merge insert sql failed, err: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return lineItem + lineAppend, nil
|
||||
}
|
@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -296,7 +297,11 @@ func loadImageTag() (string, error) {
|
||||
return itemTag, nil
|
||||
}
|
||||
|
||||
itemTag = "postgres:16.1-alpine"
|
||||
sort.Strings(versions)
|
||||
if len(versions) != 0 {
|
||||
itemTag = versions[len(versions)-1]
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
defer cancel()
|
||||
if _, err := client.ImagePull(ctx, itemTag, image.PullOptions{}); err != nil {
|
||||
|
1
backend/router/entry_xpack.go
Symbolic link
1
backend/router/entry_xpack.go
Symbolic link
@ -0,0 +1 @@
|
||||
/usr/songliu/xpack-backend/other/entry_xpack.go
|
1
backend/server/init_xpack.go
Symbolic link
1
backend/server/init_xpack.go
Symbolic link
@ -0,0 +1 @@
|
||||
/usr/songliu/xpack-backend/other/init_xpack.go
|
@ -2,7 +2,7 @@ package dto
|
||||
|
||||
type SearchCommandWithPage struct {
|
||||
PageInfo
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name command created_at"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name command createdAt"`
|
||||
Order string `json:"order" validate:"required,oneof=null ascending descending"`
|
||||
GroupID uint `json:"groupID"`
|
||||
Type string `josn:"type" validate:"required,oneof=redis command"`
|
||||
|
@ -42,7 +42,7 @@ type LoginLog struct {
|
||||
Agent string `json:"agent"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type CleanLog struct {
|
||||
|
@ -62,12 +62,18 @@ func WithByGroupBelong(group string) global.DBOption {
|
||||
}
|
||||
|
||||
func WithOrderBy(orderStr string) global.DBOption {
|
||||
if orderStr == "createdAt" {
|
||||
orderStr = "created_at"
|
||||
}
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Order(orderStr)
|
||||
}
|
||||
}
|
||||
|
||||
func WithOrderRuleBy(orderBy, order string) global.DBOption {
|
||||
if orderBy == "createdAt" {
|
||||
orderBy = "created_at"
|
||||
}
|
||||
switch order {
|
||||
case constant.OrderDesc:
|
||||
order = "desc"
|
||||
|
@ -289,7 +289,7 @@ func (u *SettingService) LoadFromCert() (*dto.SSLInfo, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "import":
|
||||
case "import-paste", "import-local":
|
||||
data, err = loadInfoFromCert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZone())
|
||||
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
global.Cron = cron.New(cron.WithLocation(nyc), cron.WithChain(cron.Recover(cron.DefaultLogger)), cron.WithChain(cron.DelayIfStillRunning(cron.DefaultLogger)))
|
||||
|
||||
_ = service.StartRefreshForToken()
|
||||
|
@ -3,6 +3,10 @@ package psession
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/core/constant"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/glebarez/sqlite"
|
||||
@ -11,9 +15,6 @@ import (
|
||||
"github.com/wader/gormstore/v2"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SessionUser struct {
|
||||
@ -99,6 +100,6 @@ func (p *PSession) Delete(c *gin.Context) error {
|
||||
}
|
||||
|
||||
func (p *PSession) Clean() error {
|
||||
p.db.Debug().Table("sessions").Where("1=1").Delete(nil)
|
||||
p.db.Table("sessions").Where("1=1").Delete(nil)
|
||||
return nil
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -92,12 +93,21 @@ func OperationLog() gin.HandlerFunc {
|
||||
}
|
||||
for key, value := range formatMap {
|
||||
if strings.Contains(operationDic.FormatEN, "["+key+"]") {
|
||||
if arrays, ok := value.([]string); ok {
|
||||
operationDic.FormatZH = strings.ReplaceAll(operationDic.FormatZH, "["+key+"]", fmt.Sprintf("[%v]", strings.Join(arrays, ",")))
|
||||
operationDic.FormatEN = strings.ReplaceAll(operationDic.FormatEN, "["+key+"]", fmt.Sprintf("[%v]", strings.Join(arrays, ",")))
|
||||
} else {
|
||||
t := reflect.TypeOf(value)
|
||||
if t.Kind() != reflect.Array && t.Kind() != reflect.Slice {
|
||||
operationDic.FormatZH = strings.ReplaceAll(operationDic.FormatZH, "["+key+"]", fmt.Sprintf("[%v]", value))
|
||||
operationDic.FormatEN = strings.ReplaceAll(operationDic.FormatEN, "["+key+"]", fmt.Sprintf("[%v]", value))
|
||||
} else {
|
||||
val := reflect.ValueOf(value)
|
||||
length := val.Len()
|
||||
|
||||
var elements []string
|
||||
for i := 0; i < length; i++ {
|
||||
element := val.Index(i).Interface().(string)
|
||||
elements = append(elements, element)
|
||||
}
|
||||
operationDic.FormatZH = strings.ReplaceAll(operationDic.FormatZH, "["+key+"]", fmt.Sprintf("[%v]", strings.Join(elements, ",")))
|
||||
operationDic.FormatEN = strings.ReplaceAll(operationDic.FormatEN, "["+key+"]", fmt.Sprintf("[%v]", strings.Join(elements, ",")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ func PasswordExpired() gin.HandlerFunc {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypePasswordExpired, err)
|
||||
return
|
||||
}
|
||||
loc, _ := time.LoadLocation(common.LoadTimeZone())
|
||||
loc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||
expiredTime, err := time.ParseInLocation(constant.DateTimeLayout, extime.Value, loc)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodePasswordExpired, constant.ErrTypePasswordExpired, err)
|
||||
|
@ -26,12 +26,16 @@ func NewS3Client(vars map[string]interface{}) (*s3Client, error) {
|
||||
if len(scType) == 0 {
|
||||
scType = "Standard"
|
||||
}
|
||||
mode := loadParamFromVars("mode", vars)
|
||||
if len(mode) == 0 {
|
||||
mode = "virtual hosted"
|
||||
}
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
|
||||
Endpoint: aws.String(endpoint),
|
||||
Region: aws.String(region),
|
||||
DisableSSL: aws.Bool(true),
|
||||
S3ForcePathStyle: aws.Bool(false),
|
||||
S3ForcePathStyle: aws.Bool(mode == "path"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -34,12 +34,23 @@ func RandStrAndNum(n int) string {
|
||||
return (string(b))
|
||||
}
|
||||
|
||||
func LoadTimeZone() string {
|
||||
loc := time.Now().Location()
|
||||
if _, err := time.LoadLocation(loc.String()); err != nil {
|
||||
return "Asia/Shanghai"
|
||||
func LoadTimeZoneByCmd() string {
|
||||
loc := time.Now().Location().String()
|
||||
if _, err := time.LoadLocation(loc); err != nil {
|
||||
loc = "Asia/Shanghai"
|
||||
}
|
||||
return loc.String()
|
||||
std, err := cmd.Exec("timedatectl | grep 'Time zone'")
|
||||
if err != nil {
|
||||
return loc
|
||||
}
|
||||
fields := strings.Fields(string(std))
|
||||
if len(fields) != 5 {
|
||||
return loc
|
||||
}
|
||||
if _, err := time.LoadLocation(fields[2]); err != nil {
|
||||
return loc
|
||||
}
|
||||
return fields[2]
|
||||
}
|
||||
|
||||
func ScanPort(port int) bool {
|
||||
|
@ -42,7 +42,7 @@
|
||||
"highlight.js": "^11.9.0",
|
||||
"js-base64": "^3.7.7",
|
||||
"md-editor-v3": "^2.11.3",
|
||||
"monaco-editor": "^0.34.1",
|
||||
"monaco-editor": "^0.50.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^1.6.1",
|
||||
|
@ -8,6 +8,7 @@
|
||||
import { reactive, computed, ref, nextTick, provide } from 'vue';
|
||||
import { GlobalStore } from '@/store';
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn';
|
||||
import zhTw from 'element-plus/es/locale/lang/zh-tw';
|
||||
import en from 'element-plus/es/locale/lang/en';
|
||||
import { useTheme } from '@/global/use-theme';
|
||||
useTheme();
|
||||
@ -19,6 +20,7 @@ const config = reactive({
|
||||
|
||||
const i18nLocale = computed(() => {
|
||||
if (globalStore.language === 'zh') return zhCn;
|
||||
if (globalStore.language === 'tw') return zhTw;
|
||||
if (globalStore.language === 'en') return en;
|
||||
return zhCn;
|
||||
});
|
||||
|
@ -19,6 +19,7 @@ export namespace Backup {
|
||||
credential: string;
|
||||
rememberAuth: boolean;
|
||||
backupPath: string;
|
||||
bucketInput: boolean;
|
||||
vars: string;
|
||||
varsJson: object;
|
||||
createdAt: Date;
|
||||
|
@ -268,6 +268,8 @@ export namespace Container {
|
||||
path: string;
|
||||
containers: Array<ComposeContainer>;
|
||||
expand: boolean;
|
||||
envStr: string;
|
||||
env: Array<string>;
|
||||
}
|
||||
export interface ComposeContainer {
|
||||
name: string;
|
||||
|
@ -2,67 +2,68 @@
|
||||
<div>
|
||||
<div class="app-status" v-if="data.isExist">
|
||||
<el-card>
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
<div class="flex w-full flex-col gap-4 md:flex-row">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<el-tag effect="dark" type="success">{{ data.app }}</el-tag>
|
||||
<Status :key="refresh" :status="data.status"></Status>
|
||||
<el-tag>{{ $t('app.version') }}{{ $t('commons.colon') }}{{ data.version }}</el-tag>
|
||||
</div>
|
||||
<div>
|
||||
<Status class="status-content" :key="refresh" :status="data.status"></Status>
|
||||
</div>
|
||||
<div>
|
||||
<el-tag class="status-content">{{ $t('app.version') }}:{{ data.version }}</el-tag>
|
||||
</div>
|
||||
<div>
|
||||
<span class="buttons">
|
||||
<el-button
|
||||
type="primary"
|
||||
v-if="data.status != 'Running'"
|
||||
link
|
||||
@click="onOperate('start')"
|
||||
:disabled="data.status === 'Installing'"
|
||||
>
|
||||
{{ $t('app.start') }}
|
||||
</el-button>
|
||||
<el-button type="primary" v-if="data.status === 'Running'" link @click="onOperate('stop')">
|
||||
{{ $t('app.stop') }}
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:disabled="data.status === 'Installing'"
|
||||
@click="onOperate('restart')"
|
||||
>
|
||||
{{ $t('app.restart') }}
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
v-if="data.app === 'OpenResty'"
|
||||
@click="onOperate('reload')"
|
||||
:disabled="data.status !== 'Running'"
|
||||
>
|
||||
{{ $t('app.reload') }}
|
||||
</el-button>
|
||||
<el-divider v-if="data.app === 'OpenResty'" direction="vertical" />
|
||||
<el-button type="primary" @click="setting" link :disabled="data.status === 'Installing'">
|
||||
{{ $t('commons.button.set') }}
|
||||
</el-button>
|
||||
<el-divider v-if="data.app === 'OpenResty'" direction="vertical" />
|
||||
<el-button
|
||||
v-if="data.app === 'OpenResty'"
|
||||
type="primary"
|
||||
@click="clear"
|
||||
link
|
||||
:disabled="
|
||||
data.status === 'Installing' ||
|
||||
(data.status !== 'Running' && data.app === 'OpenResty')
|
||||
"
|
||||
>
|
||||
{{ $t('nginx.clearProxyCache') }}
|
||||
</el-button>
|
||||
</span>
|
||||
|
||||
<div class="mt-0.5">
|
||||
<el-button
|
||||
type="primary"
|
||||
v-if="data.status != 'Running'"
|
||||
link
|
||||
@click="onOperate('start')"
|
||||
:disabled="data.status === 'Installing'"
|
||||
>
|
||||
{{ $t('app.start') }}
|
||||
</el-button>
|
||||
<el-button type="primary" v-if="data.status === 'Running'" link @click="onOperate('stop')">
|
||||
{{ $t('app.stop') }}
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:disabled="data.status === 'Installing'"
|
||||
@click="onOperate('restart')"
|
||||
>
|
||||
{{ $t('app.restart') }}
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
v-if="data.app === 'OpenResty'"
|
||||
@click="onOperate('reload')"
|
||||
:disabled="data.status !== 'Running'"
|
||||
>
|
||||
{{ $t('app.reload') }}
|
||||
</el-button>
|
||||
<el-divider v-if="data.app === 'OpenResty'" direction="vertical" />
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="setting"
|
||||
link
|
||||
:disabled="
|
||||
data.status === 'Installing' || (data.status !== 'Running' && data.app === 'OpenResty')
|
||||
"
|
||||
>
|
||||
{{ $t('commons.button.set') }}
|
||||
</el-button>
|
||||
<el-divider v-if="data.app === 'OpenResty'" direction="vertical" />
|
||||
<el-button
|
||||
v-if="data.app === 'OpenResty'"
|
||||
type="primary"
|
||||
@click="clear"
|
||||
link
|
||||
:disabled="
|
||||
data.status === 'Installing' || (data.status !== 'Running' && data.app === 'OpenResty')
|
||||
"
|
||||
>
|
||||
{{ $t('nginx.clearProxyCache') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="ml-5" v-if="key === 'openresty' && (httpPort != 80 || httpsPort != 443)">
|
||||
@ -74,20 +75,40 @@
|
||||
<el-alert
|
||||
:title="$t('app.checkTitle')"
|
||||
:closable="false"
|
||||
center
|
||||
type="warning"
|
||||
show-icon
|
||||
class="h-8"
|
||||
class="h-6 check-title"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<div v-if="!data.isExist && !isDB()">
|
||||
<LayoutContent :title="getTitle(key)" :divider="true">
|
||||
<template #main>
|
||||
<div class="app-warn">
|
||||
<div class="flex flex-col gap-2 items-center justify-center w-full sm:flex-row">
|
||||
<div>{{ $t('app.checkInstalledWarn', [data.app]) }}</div>
|
||||
<span @click="goRouter(key)" class="flex items-center justify-center gap-0.5">
|
||||
<el-icon><Position /></el-icon>
|
||||
{{ $t('database.goInstall') }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<img src="@/assets/images/no_app.svg" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { CheckAppInstalled, InstalledOp } from '@/api/modules/app';
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import router from '@/routers';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import Status from '@/components/status/index.vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import i18n from '@/lang';
|
||||
@ -105,21 +126,6 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.appKey,
|
||||
(val) => {
|
||||
key.value = val;
|
||||
onCheck();
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => props.appName,
|
||||
(val) => {
|
||||
name.value = val;
|
||||
onCheck();
|
||||
},
|
||||
);
|
||||
|
||||
let key = ref('');
|
||||
let name = ref('');
|
||||
|
||||
@ -145,8 +151,16 @@ const setting = () => {
|
||||
em('setting', false);
|
||||
};
|
||||
|
||||
const onCheck = async () => {
|
||||
await CheckAppInstalled(key.value, name.value)
|
||||
const goRouter = async (key: string) => {
|
||||
router.push({ name: 'AppAll', query: { install: key } });
|
||||
};
|
||||
|
||||
const isDB = () => {
|
||||
return key.value === 'mysql' || key.value === 'mariadb' || key.value === 'postgresql';
|
||||
};
|
||||
|
||||
const onCheck = async (key: any, name: any) => {
|
||||
await CheckAppInstalled(key, name)
|
||||
.then((res) => {
|
||||
data.value = res.data;
|
||||
em('isExist', res.data);
|
||||
@ -175,15 +189,11 @@ const clear = () => {
|
||||
const onOperate = async (operation: string) => {
|
||||
em('update:maskShow', false);
|
||||
operateReq.operate = operation;
|
||||
ElMessageBox.confirm(
|
||||
i18n.global.t('app.operatorHelper', [i18n.global.t('app.' + operation)]),
|
||||
i18n.global.t('app.' + operation),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
},
|
||||
)
|
||||
ElMessageBox.confirm(i18n.global.t(`app.${operation}OperatorHelper`), i18n.global.t('app.' + operation), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
})
|
||||
.then(() => {
|
||||
em('update:maskShow', true);
|
||||
em('update:loading', true);
|
||||
@ -192,7 +202,7 @@ const onOperate = async (operation: string) => {
|
||||
.then(() => {
|
||||
em('update:loading', false);
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
onCheck();
|
||||
onCheck(key.value, name.value);
|
||||
em('after');
|
||||
})
|
||||
.catch(() => {
|
||||
@ -204,9 +214,35 @@ const onOperate = async (operation: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getTitle = (key: string) => {
|
||||
switch (key) {
|
||||
case 'openresty':
|
||||
return i18n.global.t('website.website', 2);
|
||||
case 'mysql':
|
||||
return 'MySQL ' + i18n.global.t('menu.database').toLowerCase();
|
||||
case 'postgresql':
|
||||
return 'PostgreSQL ' + i18n.global.t('menu.database').toLowerCase();
|
||||
case 'redis':
|
||||
return 'Redis ' + i18n.global.t('menu.database').toLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
key.value = props.appKey;
|
||||
name.value = props.appName;
|
||||
onCheck();
|
||||
onCheck(key.value, name.value);
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
onCheck,
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.check-title {
|
||||
color: var(--el-color-warning);
|
||||
border: 1px solid var(--el-color-warning);
|
||||
background-color: transparent;
|
||||
padding: 8px 8px;
|
||||
width: 70px;
|
||||
}
|
||||
</style>
|
||||
|
@ -79,15 +79,14 @@ const logSearch = reactive({
|
||||
const handleClose = () => {
|
||||
logSocket.value?.send('close conn');
|
||||
open.value = false;
|
||||
globalStore.isFullScreen = false;
|
||||
};
|
||||
|
||||
function toggleFullscreen() {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.toggle();
|
||||
}
|
||||
globalStore.isFullScreen = !globalStore.isFullScreen;
|
||||
}
|
||||
const loadTooltip = () => {
|
||||
return i18n.global.t('commons.button.' + (screenfull.isFullscreen ? 'quitFullscreen' : 'fullscreen'));
|
||||
return i18n.global.t('commons.button.' + (globalStore.isFullScreen ? 'quitFullscreen' : 'fullscreen'));
|
||||
};
|
||||
|
||||
watch(logVisible, (val) => {
|
||||
@ -204,6 +203,9 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.fullScreen {
|
||||
border: none;
|
||||
}
|
||||
.selectWidth {
|
||||
width: 200px;
|
||||
}
|
||||
|
@ -47,8 +47,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, useSlots } from 'vue';
|
||||
defineOptions({ name: 'DrawerPro' });
|
||||
import screenfull from 'screenfull';
|
||||
import i18n from '@/lang';
|
||||
import { GlobalStore } from '@/store';
|
||||
const globalStore = GlobalStore();
|
||||
|
||||
const props = defineProps({
|
||||
header: String,
|
||||
@ -111,14 +112,13 @@ const handleBack = () => {
|
||||
|
||||
const closePage = () => {
|
||||
localOpenPage.value = false;
|
||||
globalStore.isFullScreen = false;
|
||||
};
|
||||
|
||||
function toggleFullscreen() {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.toggle();
|
||||
}
|
||||
globalStore.isFullScreen = !globalStore.isFullScreen;
|
||||
}
|
||||
const loadTooltip = () => {
|
||||
return i18n.global.t('commons.button.' + (screenfull.isFullscreen ? 'quitFullscreen' : 'fullscreen'));
|
||||
return i18n.global.t('commons.button.' + (globalStore.isFullScreen ? 'quitFullscreen' : 'fullscreen'));
|
||||
};
|
||||
</script>
|
||||
|
@ -11,7 +11,7 @@
|
||||
</template>
|
||||
|
||||
<ComplexTable :data="data" @search="search()">
|
||||
<template #toolbar>
|
||||
<template #leftToolBar>
|
||||
<el-button type="primary" @click="openCreate">{{ $t('website.createGroup') }}</el-button>
|
||||
</template>
|
||||
<el-table-column :label="$t('commons.table.name')" prop="name">
|
||||
|
@ -22,33 +22,37 @@
|
||||
<div class="content-container__title">
|
||||
<slot name="title">
|
||||
<div v-if="showBack">
|
||||
<div class="flex justify-between">
|
||||
<back-button
|
||||
:path="backPath"
|
||||
:name="backName"
|
||||
:to="backTo"
|
||||
:header="title"
|
||||
:reload="reload"
|
||||
>
|
||||
<template v-if="slots.leftToolBar" #buttons>
|
||||
<slot name="leftToolBar" v-if="slots.leftToolBar"></slot>
|
||||
</template>
|
||||
</back-button>
|
||||
<div>
|
||||
<div class="flex flex-wrap gap-4 sm:justify-between">
|
||||
<div class="flex gap-2 flex-wrap items-center justify-start">
|
||||
<back-button
|
||||
:path="backPath"
|
||||
:name="backName"
|
||||
:to="backTo"
|
||||
:header="title"
|
||||
:reload="reload"
|
||||
>
|
||||
<template v-if="slots.leftToolBar" #buttons>
|
||||
<slot name="leftToolBar" v-if="slots.leftToolBar"></slot>
|
||||
</template>
|
||||
</back-button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<slot name="rightToolBar" v-if="slots.rightToolBar"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between" v-else>
|
||||
<div>
|
||||
<!-- {{ title }} -->
|
||||
<!-- <el-divider direction="vertical" v-if="slots.leftToolBar || slots.buttons" /> -->
|
||||
<slot name="leftToolBar" v-if="slots.leftToolBar"></slot>
|
||||
<slot name="buttons" v-if="slots.buttons"></slot>
|
||||
</div>
|
||||
<div class="flex justify-end" v-if="slots.rightToolBar || slots.rightButton">
|
||||
<slot name="rightToolBar"></slot>
|
||||
<slot name="rightButton"></slot>
|
||||
<div v-else>
|
||||
<div class="flex flex-wrap gap-4 sm:justify-between">
|
||||
<div class="flex gap-2 flex-wrap items-center justify-start">
|
||||
{{ title }}
|
||||
<el-divider direction="vertical" v-if="slots.leftToolBar || slots.buttons" />
|
||||
<slot name="leftToolBar" v-if="slots.leftToolBar"></slot>
|
||||
<slot name="buttons" v-if="slots.buttons"></slot>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3" v-if="slots.rightToolBar || slots.rightButton">
|
||||
<slot name="rightToolBar"></slot>
|
||||
<slot name="rightButton"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -75,7 +79,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useSlots } from 'vue';
|
||||
import BackButton from '@/components/back-button/index.vue';
|
||||
// import BackButton from '@/components/back-button/index.vue';
|
||||
import FormButton from './form-button.vue';
|
||||
defineOptions({ name: 'LayoutContent' });
|
||||
const slots = useSlots();
|
||||
@ -111,6 +115,9 @@ const showBack = computed(() => {
|
||||
.content-container__title {
|
||||
font-weight: 400;
|
||||
font-size: 18px;
|
||||
.el-button + .el-button {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.content-container_form {
|
||||
|
@ -4,6 +4,7 @@
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:before-close="handleClose"
|
||||
:size="globalStore.isFullScreen ? '100%' : '50%'"
|
||||
>
|
||||
<template #header>
|
||||
@ -21,7 +22,7 @@
|
||||
</el-drawer>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import LogFile from '@/components/log-file/index.vue';
|
||||
import { GlobalStore } from '@/store';
|
||||
import screenfull from 'screenfull';
|
||||
@ -46,19 +47,22 @@ const open = ref(false);
|
||||
const config = ref();
|
||||
const em = defineEmits(['close']);
|
||||
|
||||
const handleClose = (search: boolean) => {
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
em('close', search);
|
||||
em('close', false);
|
||||
globalStore.isFullScreen = false;
|
||||
};
|
||||
|
||||
watch(open, (val) => {
|
||||
if (screenfull.isEnabled && !val && !mobile.value) screenfull.exit();
|
||||
});
|
||||
|
||||
function toggleFullscreen() {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.toggle();
|
||||
}
|
||||
globalStore.isFullScreen = !globalStore.isFullScreen;
|
||||
}
|
||||
|
||||
const loadTooltip = () => {
|
||||
return i18n.global.t('commons.button.' + (screenfull.isFullscreen ? 'quitFullscreen' : 'fullscreen'));
|
||||
return i18n.global.t('commons.button.' + (globalStore.isFullScreen ? 'quitFullscreen' : 'fullscreen'));
|
||||
};
|
||||
|
||||
const acceptParams = (props: LogProps) => {
|
||||
|
@ -1,20 +1,24 @@
|
||||
<template>
|
||||
<el-card class="router_card">
|
||||
<el-radio-group v-model="activeName" @change="handleChange">
|
||||
<el-radio-button
|
||||
class="router_card_button"
|
||||
:label="button.label"
|
||||
:value="button.label"
|
||||
v-for="(button, index) in buttonArray"
|
||||
size="large"
|
||||
:key="index"
|
||||
>
|
||||
<el-badge :value="button.count" v-if="button.count" is-dot>
|
||||
<span>{{ button.label }}</span>
|
||||
</el-badge>
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<slot name="route-button"></slot>
|
||||
<div class="flex w-full flex-col items-center md:justify-between md:flex-row">
|
||||
<el-radio-group v-model="activeName" @change="handleChange">
|
||||
<el-radio-button
|
||||
class="router_card_button"
|
||||
:label="button.label"
|
||||
:value="button.label"
|
||||
v-for="(button, index) in buttonArray"
|
||||
size="large"
|
||||
:key="index"
|
||||
>
|
||||
<el-badge :value="button.count" v-if="button.count" is-dot>
|
||||
<span>{{ button.label }}</span>
|
||||
</el-badge>
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
<div class="flex flex-row gap-2 md:flex-col lg:flex-row">
|
||||
<slot name="route-button"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
|
@ -1,37 +1,45 @@
|
||||
<template>
|
||||
<div class="flx-center">
|
||||
<span v-if="props.footer">
|
||||
<el-button type="primary" link @click="toForum">
|
||||
<span class="font-normal">{{ $t('setting.forum') }}</span>
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button type="primary" link @click="toDoc">
|
||||
<span class="font-normal">{{ $t('setting.doc2') }}</span>
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button type="primary" link @click="toGithub">
|
||||
<span class="font-normal">{{ $t('setting.project') }}</span>
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
</span>
|
||||
<el-button type="primary" link @click="toHalo">
|
||||
<span class="font-normal">{{ isMasterProductPro ? $t('license.pro') : $t('license.community') }}</span>
|
||||
</el-button>
|
||||
<span class="version">{{ version }}</span>
|
||||
<el-badge is-dot style="margin-top: -3px" v-if="version !== 'Waiting' && globalStore.hasNewVersion">
|
||||
<el-button type="primary" link @click="onLoadUpgradeInfo">
|
||||
<span class="font-normal">({{ $t('setting.hasNewVersion') }})</span>
|
||||
</el-button>
|
||||
</el-badge>
|
||||
<el-button
|
||||
v-if="version !== 'Waiting' && !globalStore.hasNewVersion"
|
||||
type="primary"
|
||||
link
|
||||
@click="onLoadUpgradeInfo"
|
||||
>
|
||||
<span>({{ $t('setting.upgradeCheck') }})</span>
|
||||
</el-button>
|
||||
<el-tag v-if="version === 'Waiting'" round style="margin-left: 10px">{{ $t('setting.upgrading') }}</el-tag>
|
||||
<div>
|
||||
<div class="flex w-full flex-col gap-2 md:flex-row items-center">
|
||||
<div class="flex flex-wrap items-center" v-if="props.footer">
|
||||
<el-button type="primary" link @click="toForum">
|
||||
<span class="font-normal">{{ $t('setting.forum') }}</span>
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button type="primary" link @click="toDoc">
|
||||
<span class="font-normal">{{ $t('setting.doc2') }}</span>
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button type="primary" link @click="toGithub">
|
||||
<span class="font-normal">{{ $t('setting.project') }}</span>
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center">
|
||||
<el-button type="primary" link @click="toHalo">
|
||||
<span class="font-normal">
|
||||
{{ isMasterProductPro ? $t('license.pro') : $t('license.community') }}
|
||||
</span>
|
||||
</el-button>
|
||||
<span class="version" @click="copyText(version)">{{ version }}</span>
|
||||
<el-badge is-dot style="margin-top: -3px" v-if="version !== 'Waiting' && globalStore.hasNewVersion">
|
||||
<el-button type="primary" link @click="onLoadUpgradeInfo">
|
||||
<span class="font-normal">({{ $t('setting.hasNewVersion') }})</span>
|
||||
</el-button>
|
||||
</el-badge>
|
||||
<el-button
|
||||
v-if="version !== 'Waiting' && !globalStore.hasNewVersion"
|
||||
type="primary"
|
||||
link
|
||||
@click="onLoadUpgradeInfo"
|
||||
>
|
||||
<span>({{ $t('setting.upgradeCheck') }})</span>
|
||||
</el-button>
|
||||
<el-tag v-if="version === 'Waiting'" round style="margin-left: 10px">
|
||||
{{ $t('setting.upgrading') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Upgrade ref="upgradeRef" @search="search" />
|
||||
</div>
|
||||
@ -42,6 +50,7 @@ import { getSettingInfo, loadUpgradeInfo } from '@/api/modules/setting';
|
||||
import Upgrade from '@/components/system-upgrade/upgrade/index.vue';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { copyText } from '@/utils/util';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { GlobalStore } from '@/store';
|
||||
|
||||
@ -124,24 +133,4 @@ onMounted(() => {
|
||||
text-decoration: none;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.line-height {
|
||||
line-height: 25px;
|
||||
}
|
||||
.panel-MdEditor {
|
||||
height: calc(100vh - 330px);
|
||||
.tag {
|
||||
margin-top: -6px;
|
||||
margin-left: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
:deep(.md-editor-preview) {
|
||||
font-size: 14px;
|
||||
}
|
||||
:deep(.default-theme h2) {
|
||||
color: var(--dark-gold-base-color);
|
||||
margin: 13px, 0;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -15,7 +15,15 @@
|
||||
<div class="mb-4" v-if="type === 'website'">
|
||||
<el-alert :closable="false" type="warning" :title="$t('website.websiteBackupWarn')"></el-alert>
|
||||
</div>
|
||||
<el-upload ref="uploadRef" drag :on-change="fileOnChange" class="upload-demo" :auto-upload="false">
|
||||
<el-upload
|
||||
:limit="1"
|
||||
ref="uploadRef"
|
||||
drag
|
||||
:on-exceed="handleExceed"
|
||||
:on-change="fileOnChange"
|
||||
class="upload-demo"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
{{ $t('database.dropHelper') }}
|
||||
@ -46,7 +54,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-button :disabled="isUpload" v-if="uploaderFiles.length === 1" icon="Upload" @click="onSubmit">
|
||||
<el-button :disabled="isUpload || uploaderFiles.length !== 1" icon="Upload" @click="onSubmit">
|
||||
{{ $t('commons.button.upload') }}
|
||||
</el-button>
|
||||
|
||||
@ -57,7 +65,7 @@
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
>
|
||||
<template #toolbar>
|
||||
<template #leftToolBar>
|
||||
<el-button
|
||||
class="ml-2.5"
|
||||
plain
|
||||
@ -130,9 +138,9 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { computeSize } from '@/utils/util';
|
||||
import { computeSize, newUUID } from '@/utils/util';
|
||||
import i18n from '@/lang';
|
||||
import { UploadFile, UploadFiles, UploadInstance } from 'element-plus';
|
||||
import { UploadFile, UploadFiles, UploadInstance, UploadProps, UploadRawFile, genFileId } from 'element-plus';
|
||||
import { File } from '@/api/interface/file';
|
||||
import { BatchDeleteFile, CheckFile, ChunkUploadFileData, GetUploadList } from '@/api/modules/files';
|
||||
import { loadBaseDir } from '@/api/modules/setting';
|
||||
@ -189,11 +197,11 @@ const acceptParams = async (params: DialogProps): Promise<void> => {
|
||||
break;
|
||||
case 'website':
|
||||
title.value = name.value;
|
||||
baseDir.value = `${pathRes.data}/uploads/database/${type.value}/${detailName.value}/`;
|
||||
baseDir.value = `${pathRes.data}/uploads/website/${type.value}/${detailName.value}/`;
|
||||
break;
|
||||
case 'app':
|
||||
title.value = name.value;
|
||||
baseDir.value = `${pathRes.data}/uploads/database/${type.value}/${name.value}/`;
|
||||
baseDir.value = `${pathRes.data}/uploads/app/${type.value}/${name.value}/`;
|
||||
}
|
||||
upVisible.value = true;
|
||||
search();
|
||||
@ -218,6 +226,7 @@ const onHandleRecover = async (row?: any) => {
|
||||
detailName: detailName.value,
|
||||
file: baseDir.value + row.name,
|
||||
secret: secret.value,
|
||||
taskID: newUUID(),
|
||||
};
|
||||
loading.value = true;
|
||||
await handleRecoverByUpload(params)
|
||||
@ -236,8 +245,8 @@ const onHandleRecover = async (row?: any) => {
|
||||
const onRecover = async (row: File.File) => {
|
||||
if (type.value !== 'app' && type.value !== 'website') {
|
||||
ElMessageBox.confirm(
|
||||
i18n.global.t('commons.msg.backupHelper', [name.value + '( ' + detailName.value + ' )']),
|
||||
i18n.global.t('commons.button.backup'),
|
||||
i18n.global.t('commons.msg.recoverHelper', [row.name]),
|
||||
i18n.global.t('commons.button.recover'),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
@ -281,6 +290,13 @@ const handleBackupClose = () => {
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
const handleExceed: UploadProps['onExceed'] = (files) => {
|
||||
uploadRef.value!.clearFiles();
|
||||
const file = files[0] as UploadRawFile;
|
||||
file.uid = genFileId();
|
||||
uploadRef.value!.handleStart(file);
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (uploaderFiles.value.length !== 1) {
|
||||
return;
|
||||
|
@ -228,7 +228,7 @@ const checkImageName = (rule: any, value: any, callback: any) => {
|
||||
if (value === '' || typeof value === 'undefined' || value == null) {
|
||||
callback(new Error(i18n.global.t('commons.rule.imageName')));
|
||||
} else {
|
||||
const reg = /^[a-zA-Z0-9]{1}[a-z:A-Z0-9_/.-]{0,149}$/;
|
||||
const reg = /^[a-zA-Z0-9]{1}[a-z:@A-Z0-9_/.-]{0,256}$/;
|
||||
if (!reg.test(value) && value !== '') {
|
||||
callback(new Error(i18n.global.t('commons.rule.imageName')));
|
||||
} else {
|
||||
|
@ -11,6 +11,7 @@ const message = {
|
||||
true: 'true',
|
||||
false: 'false',
|
||||
example: 'e.g.:',
|
||||
fit2cloud: 'FIT2CLOUD',
|
||||
button: {
|
||||
prev: 'Previous',
|
||||
next: 'Next',
|
||||
@ -59,8 +60,8 @@ const message = {
|
||||
copy: 'Copy',
|
||||
random: 'Random',
|
||||
uninstall: 'Uninstall',
|
||||
fullscreen: 'Fullscreen',
|
||||
quitFullscreen: 'Quit Fullscreen',
|
||||
fullscreen: 'WebsiteFullscreen',
|
||||
quitFullscreen: 'Quit WebsiteFullscreen',
|
||||
update: 'Edit',
|
||||
showAll: 'Show All',
|
||||
hideSome: 'Hide Some',
|
||||
@ -198,7 +199,7 @@ const message = {
|
||||
simpleName: 'Supports non-underscore starting, English, numbers, _, length 3-30',
|
||||
simplePassword: 'Supports non-underscore starting, English, numbers, _, length 1-30',
|
||||
dbName: 'Supports non-special character starting, including English, Chinese, numbers, .-_, with a length of 1-64',
|
||||
imageName: 'Support English, numbers, :/.-_, length 1-150',
|
||||
imageName: 'Support English, numbers, :@/.-_, length 1-256',
|
||||
volumeName: 'Support English, numbers, .-_, length 2-30',
|
||||
supervisorName: 'Supports non-special characters starting with English, numbers, - and _, length 1-128',
|
||||
complexityPassword:
|
||||
@ -589,8 +590,13 @@ const message = {
|
||||
commandRule: 'Please enter the correct docker run container creation command!',
|
||||
commandHelper: 'This command will be executed on the server to create the container. Do you want to continue?',
|
||||
edit: 'Edit container',
|
||||
updateContainerHelper:
|
||||
'Container editing requires rebuilding the container. Any data that has not been persisted will be lost. Do you want to continue?',
|
||||
updateHelper1: 'Detected that this container comes from the app store. Please note the following two points:',
|
||||
updateHelper2:
|
||||
'1. The current modifications will not be synchronized to the installed applications in the app store.',
|
||||
updateHelper3:
|
||||
'2. If you modify the application on the installed page, the currently edited content will become invalid.',
|
||||
updateHelper4:
|
||||
'Editing the container requires rebuilding, and any non-persistent data will be lost. Do you want to continue?',
|
||||
containerList: 'Container list',
|
||||
operatorHelper: '{0} will be performed on the following container, Do you want to continue?',
|
||||
operatorAppHelper:
|
||||
@ -658,7 +664,7 @@ const message = {
|
||||
containerFromAppHelper:
|
||||
'Detected that this container originates from the app store. App operations may cause current edits to be invalidated.',
|
||||
containerFromAppHelper1:
|
||||
'Click the `Settings` button in the installed applications list to enter the editing page and modify the container name.',
|
||||
'Click the [Param] button in the installed applications list to enter the editing page and modify the container name.',
|
||||
command: 'Command',
|
||||
console: 'Console Interaction',
|
||||
tty: 'TTY (-t)',
|
||||
@ -669,6 +675,8 @@ const message = {
|
||||
privileged: 'Privileged',
|
||||
privilegedHelper:
|
||||
'Allows the container to perform certain privileged operations on the host, which may increase container risks. Use with caution!',
|
||||
editComposeHelper:
|
||||
'Note: The environment variables set will be written to the 1panel.env file by default.\nIf you want to use these parameters in the container, you also need to manually add an env_file reference in the compose file.',
|
||||
|
||||
upgradeHelper: 'Repository Name/Image Name: Image Version',
|
||||
upgradeWarning2:
|
||||
@ -687,8 +695,8 @@ const message = {
|
||||
containerExample: '80 or 80-88',
|
||||
exposePort: 'Expose port',
|
||||
exposeAll: 'Expose all',
|
||||
cmdHelper: "e.g. 'nginx' '-g' 'daemon off;' OR nginx -g daemon off;",
|
||||
entrypointHelper: 'e.g. /bin/sh -c',
|
||||
cmdHelper: 'e.g. nginx -g "daemon off;"',
|
||||
entrypointHelper: 'e.g. docker-entrypoint.sh',
|
||||
autoRemove: 'Auto remove',
|
||||
cpuQuota: 'NacosCPU',
|
||||
memoryLimit: 'Memory',
|
||||
@ -746,6 +754,8 @@ const message = {
|
||||
urlWarning: 'The URL prefix does not need to include http:// or https://. Please modify.',
|
||||
|
||||
network: 'Network',
|
||||
networkHelper:
|
||||
'Deleting the 1panel-network container network will affect the normal use of some applications and runtime environments. Do you want to continue?',
|
||||
createNetwork: 'Create',
|
||||
networkName: 'Name',
|
||||
driver: 'Driver',
|
||||
@ -785,12 +795,13 @@ const message = {
|
||||
composeHelper:
|
||||
'The composition created through 1Panel editor or template will be saved in the {0}/docker/compose directory.',
|
||||
deleteFile: 'Delete file',
|
||||
allDelete: 'Permanently Delete',
|
||||
deleteComposeHelper:
|
||||
'Delete all files in the {0} directory, including persistent files in this directory. Please proceed with caution!',
|
||||
deleteCompose: '" Delete this composition.',
|
||||
'1. Delete container orchestration records \n2. Delete all container orchestration files, including configuration and persistent files',
|
||||
apps: 'Apps',
|
||||
local: 'Local',
|
||||
createCompose: 'Create',
|
||||
composeDirectory: 'Compose Directory',
|
||||
template: 'Template',
|
||||
composeTemplate: 'Compose template',
|
||||
createComposeTemplate: 'Create',
|
||||
@ -798,11 +809,16 @@ const message = {
|
||||
content: 'Content',
|
||||
contentEmpty: 'Compose content cannot be empty, please enter and try again!',
|
||||
containerNumber: 'Container number',
|
||||
containerStatus: 'Container Status',
|
||||
exited: 'Exited',
|
||||
running: 'Running',
|
||||
down: 'Down',
|
||||
up: 'Up',
|
||||
composeDetailHelper:
|
||||
'The compose is created external to 1Panel. The start and stop operations are not supported.',
|
||||
composeOperatorHelper: '{1} operation will be performed on {0}. Do you want to continue?',
|
||||
composeDownHelper:
|
||||
'This will stop and remove all containers and networks under the {0} compose. Do you want to continue?',
|
||||
|
||||
setting: 'Setting',
|
||||
goSetting: 'Go to edit',
|
||||
@ -1451,6 +1467,7 @@ const message = {
|
||||
LOCAL: 'Server Disks',
|
||||
OSS: 'Ali OSS',
|
||||
S3: 'Amazon S3',
|
||||
mode: 'Mode',
|
||||
MINIO: 'MINIO',
|
||||
SFTP: 'SFTP',
|
||||
WebDAV: 'WebDAV',
|
||||
@ -1580,6 +1597,7 @@ const message = {
|
||||
allowIPEgs:
|
||||
'If multiple ip authorizations exist, newlines need to be displayed. For example, \n172.16.10.111 \n172.16.10.0/24',
|
||||
mfa: 'MFA',
|
||||
mfaClose: 'Disabling MFA will reduce the security of the service. Do you want to continue?',
|
||||
secret: 'Secret',
|
||||
mfaInterval: 'Refresh interval (s)',
|
||||
mfaTitleHelper:
|
||||
@ -1870,7 +1888,7 @@ const message = {
|
||||
gotoInstalled: 'Go to install',
|
||||
search: 'Search',
|
||||
limitHelper: 'The application has already been installed, does not support repeated installation',
|
||||
deleteHelper: '{0} has been associated with the following resources and cannot be deleted',
|
||||
deleteHelper: '{0} has been associated with the following resources. Please check and try again!',
|
||||
checkTitle: 'Prompt',
|
||||
website: 'website',
|
||||
database: 'database',
|
||||
|
@ -1,4 +1,4 @@
|
||||
import fit2cloudTwLocale from 'fit2cloud-ui-plus/src/locale/lang/zh-cn';
|
||||
import fit2cloudTwLocale from 'fit2cloud-ui-plus/src/locale/lang/zh-tw';
|
||||
let xpackTwLocale = {};
|
||||
const xpackModules = import.meta.glob('../../xpack/lang/tw.ts', { eager: true });
|
||||
if (xpackModules['../../xpack/lang/tw.ts']) {
|
||||
@ -10,6 +10,7 @@ const message = {
|
||||
true: '是',
|
||||
false: '否',
|
||||
example: '例:',
|
||||
fit2cloud: '飛致雲',
|
||||
button: {
|
||||
prev: '上一步',
|
||||
next: '下一步',
|
||||
@ -21,7 +22,7 @@ const message = {
|
||||
delete: '刪除',
|
||||
edit: '編輯',
|
||||
enable: '啟用',
|
||||
disable: '禁用',
|
||||
disable: '停用',
|
||||
confirm: '確認',
|
||||
cancel: '取消',
|
||||
reset: '重置',
|
||||
@ -58,8 +59,8 @@ const message = {
|
||||
copy: '復製',
|
||||
random: '隨機密碼',
|
||||
uninstall: '卸載',
|
||||
fullscreen: '全屏',
|
||||
quitFullscreen: '退出全屏',
|
||||
fullscreen: '網頁全屏',
|
||||
quitFullscreen: '退出網頁全屏',
|
||||
update: '編輯',
|
||||
showAll: '顯示所有',
|
||||
hideSome: '隱藏部分',
|
||||
@ -197,7 +198,7 @@ const message = {
|
||||
simpleName: '支持非底線開頭,英文、數字、_,長度3-30',
|
||||
simplePassword: '支持非底線開頭,英文、數字、_,長度1-30',
|
||||
dbName: '支持非特殊字符開頭,英文、中文、數字、.-_,長度1-64',
|
||||
imageName: '支持英文、數字、:/.-_,長度1-150',
|
||||
imageName: '支持英文、數字、:@/.-_,長度1-256',
|
||||
volumeName: '支持英文、數字、.-和_,長度2-30',
|
||||
supervisorName: '支援非特殊字元開頭,英文、數字、-和_,長度1-128',
|
||||
complexityPassword: '請輸入長度為 8-30 位,並包含字母、數字、至少兩種特殊字符的密碼組合',
|
||||
@ -571,7 +572,10 @@ const message = {
|
||||
commandRule: '請輸入正確的 docker run 容器創建命令!',
|
||||
commandHelper: '將在伺服器上執行該條命令以創建容器,是否繼續?',
|
||||
edit: '編輯容器',
|
||||
updateContainerHelper: '容器編輯需要重建容器,任何未持久化的數據將會丟失,是否繼續?',
|
||||
updateHelper1: '檢測到該容器來源於應用商店,請注意以下兩點:',
|
||||
updateHelper2: '1. 當前修改內容不會同步到應用商店的已安裝應用。',
|
||||
updateHelper3: '2. 如果在已安裝頁面修改應用,當前編輯的部分內容將失效。',
|
||||
updateHelper4: '編輯容器需要重建,任何未持久化的數據將丟失,是否繼續操作?',
|
||||
containerList: '容器列表',
|
||||
operatorHelper: '將對以下容器進行 {0} 操作,是否繼續?',
|
||||
operatorAppHelper:
|
||||
@ -634,7 +638,7 @@ const message = {
|
||||
inputIpv6: '請輸入 IPv6 地址',
|
||||
|
||||
containerFromAppHelper: '檢測到該容器來源於應用商店,應用操作可能會導致當前編輯失效',
|
||||
containerFromAppHelper1: '在已安裝應用程式列表點擊 `參數` 按鈕,進入編輯頁面即可修改容器名稱。',
|
||||
containerFromAppHelper1: '在已安裝應用程式列表點擊 [參數] 按鈕,進入編輯頁面即可修改容器名稱。',
|
||||
command: '命令',
|
||||
console: '控製臺交互',
|
||||
tty: '偽終端 ( -t )',
|
||||
@ -644,6 +648,8 @@ const message = {
|
||||
emptyUser: '為空時,將使用容器默認的用戶登錄',
|
||||
privileged: '特權模式',
|
||||
privilegedHelper: '允許容器在主機上執行某些特權操作,可能會增加容器風險,請謹慎開啟!',
|
||||
editComposeHelper:
|
||||
'注意:設置的環境變數會默認寫入 1panel.env 文件。\n若需在容器中使用這些參數,還需在 compose 文件中手動添加 env_file 引用。',
|
||||
|
||||
upgradeHelper: '倉庫名稱/鏡像名稱:鏡像版本',
|
||||
upgradeWarning2: '升級操作需要重建容器,任何未持久化的數據將會丟失,是否繼續?',
|
||||
@ -661,8 +667,8 @@ const message = {
|
||||
containerExample: '80 或者 80-88',
|
||||
exposePort: '暴露端口',
|
||||
exposeAll: '暴露所有',
|
||||
cmdHelper: "例: 'nginx' '-g' 'daemon off;' 或 nginx -g daemon off;",
|
||||
entrypointHelper: '例: /bin/sh -c',
|
||||
cmdHelper: '例: nginx -g "daemon off;"',
|
||||
entrypointHelper: '例: docker-entrypoint.sh',
|
||||
autoRemove: '容器退出後自動刪除容器',
|
||||
cpuQuota: 'CPU 限製',
|
||||
memoryLimit: '內存限製',
|
||||
@ -722,6 +728,8 @@ const message = {
|
||||
urlWarning: '路徑前綴不需要添加 http:// 或 https://,請修改',
|
||||
|
||||
network: '網絡',
|
||||
networkHelper:
|
||||
'Deleting the 1panel-network container network will affect the normal use of some applications and runtime environments. Do you want to continue?',
|
||||
createNetwork: '創建網絡',
|
||||
networkName: '網絡名',
|
||||
driver: '模式',
|
||||
@ -756,11 +764,13 @@ const message = {
|
||||
composePathHelper: '配置文件保存路徑: {0}',
|
||||
composeHelper: '通過 1Panel 編輯或者模版創建的編排,將保存在 {0}/docker/compose 路徑下',
|
||||
deleteFile: '刪除文件',
|
||||
deleteComposeHelper: '刪除 {0} 目录下所有文件,包括該文件下的持久化文件等,請謹慎操作!',
|
||||
allDelete: '徹底刪除',
|
||||
deleteComposeHelper: '1. 刪除容器編排記錄 \n2. 刪除容器編排的所有文件,包括配置文件和持久化文件',
|
||||
deleteCompose: '" 刪除此編排',
|
||||
apps: '應用商店',
|
||||
local: '本地',
|
||||
createCompose: '創建編排',
|
||||
composeDirectory: '編排目錄',
|
||||
template: '模版',
|
||||
composeTemplate: '編排模版',
|
||||
createComposeTemplate: '創建編排模版',
|
||||
@ -768,10 +778,14 @@ const message = {
|
||||
content: '內容',
|
||||
contentEmpty: '編排內容不能為空,請輸入後重試!',
|
||||
containerNumber: '容器數量',
|
||||
containerStatus: '容器狀態',
|
||||
exited: '已停止',
|
||||
running: '運行中',
|
||||
down: '刪除',
|
||||
up: '啟動',
|
||||
composeDetailHelper: '該 compose 為 1Panel 編排外部創建。暫不支持啟停操作。',
|
||||
composeOperatorHelper: '將對 {0} 進行 {1} 操作,是否繼續?',
|
||||
composeDownHelper: '將停止並刪除 {0} 編排下所有容器及網絡,是否繼續?',
|
||||
|
||||
setting: '配置',
|
||||
goSetting: '去修改',
|
||||
@ -1364,6 +1378,7 @@ const message = {
|
||||
LOCAL: '服務器磁盤',
|
||||
OSS: '阿裏雲 OSS',
|
||||
S3: '亞馬遜 S3 雲存儲',
|
||||
mode: '模式',
|
||||
MINIO: 'MINIO',
|
||||
SFTP: 'SFTP',
|
||||
WebDAV: 'WebDAV',
|
||||
@ -1554,6 +1569,7 @@ const message = {
|
||||
allowIPsHelper1: '授權 IP 為空時,則取消授權 IP',
|
||||
allowIPEgs: '當存在多個授權 IP 時,需要換行顯示,例: \n172.16.10.111 \n172.16.10.0/24',
|
||||
mfa: '兩步驗證',
|
||||
mfaClose: '關閉兩步驗證將導致服務安全性降低,是否繼續?',
|
||||
secret: '密鑰',
|
||||
mfaAlert: '兩步驗證密碼是基於當前時間生成,請確保服務器時間已同步',
|
||||
mfaHelper: '開啟後會驗證手機應用驗證碼',
|
||||
@ -1742,7 +1758,7 @@ const message = {
|
||||
gotoInstalled: '去安裝',
|
||||
search: '搜索',
|
||||
limitHelper: '該應用已安裝,不支持重復安裝',
|
||||
deleteHelper: '{0}已經關聯以下資源,無法刪除',
|
||||
deleteHelper: '{0}已經關聯以下資源,請檢查後重試!',
|
||||
checkTitle: '提示',
|
||||
website: '網站',
|
||||
database: '數據庫',
|
||||
|
@ -10,6 +10,7 @@ const message = {
|
||||
true: '是',
|
||||
false: '否',
|
||||
example: '例:',
|
||||
fit2cloud: '飞致云',
|
||||
button: {
|
||||
prev: '上一步',
|
||||
next: '下一步',
|
||||
@ -21,7 +22,7 @@ const message = {
|
||||
delete: '删除',
|
||||
edit: '编辑',
|
||||
enable: '启用',
|
||||
disable: '禁用',
|
||||
disable: '停用',
|
||||
confirm: '确认',
|
||||
cancel: '取消',
|
||||
reset: '重置',
|
||||
@ -58,8 +59,8 @@ const message = {
|
||||
copy: '复制',
|
||||
random: '随机密码',
|
||||
uninstall: '卸载',
|
||||
fullscreen: '全屏',
|
||||
quitFullscreen: '退出全屏',
|
||||
fullscreen: '网页全屏',
|
||||
quitFullscreen: '退出网页全屏',
|
||||
update: '编辑',
|
||||
showAll: '显示所有',
|
||||
hideSome: '隐藏部分',
|
||||
@ -197,7 +198,7 @@ const message = {
|
||||
simpleName: '支持非下划线开头,英文、数字、_,长度3-30',
|
||||
simplePassword: '支持非下划线开头,英文、数字、_,长度1-30',
|
||||
dbName: '支持非特殊字符开头,英文、中文、数字、.-_,长度1-64',
|
||||
imageName: '支持英文、数字、:/.-_,长度1-150',
|
||||
imageName: '支持英文、数字、:@/.-_,长度1-256',
|
||||
volumeName: '支持英文、数字、.-和_,长度2-30',
|
||||
supervisorName: '支持非特殊字符开头,英文、数字、-和_,长度1-128',
|
||||
complexityPassword: '请输入长度为 8-30 位且包含字母、数字、特殊字符至少两项的密码组合',
|
||||
@ -571,7 +572,10 @@ const message = {
|
||||
commandRule: '请输入正确的 docker run 容器创建命令!',
|
||||
commandHelper: '将在服务器上执行该条命令以创建容器,是否继续?',
|
||||
edit: '编辑容器',
|
||||
updateContainerHelper: '容器编辑需要重建容器,任何未持久化的数据将会丢失,是否继续?',
|
||||
updateHelper1: '检测到该容器来源于应用商店,请注意以下两点:',
|
||||
updateHelper2: '1. 当前修改内容不会同步到应用商店的已安装应用。',
|
||||
updateHelper3: '2. 如果在已安装页面修改应用,当前编辑的部分内容将失效。',
|
||||
updateHelper4: '编辑容器需要重建,任何未持久化的数据将丢失,是否继续操作?',
|
||||
containerList: '容器列表',
|
||||
operatorHelper: '将对以下容器进行 {0} 操作,是否继续?',
|
||||
operatorAppHelper:
|
||||
@ -635,7 +639,7 @@ const message = {
|
||||
inputIpv6: '请输入 IPv6 地址',
|
||||
|
||||
containerFromAppHelper: '检测到该容器来源于应用商店,应用操作可能会导致当前编辑失效',
|
||||
containerFromAppHelper1: '在已安装应用列表点击 `参数` 按钮,进入编辑页面即可修改容器名称。',
|
||||
containerFromAppHelper1: '在应用商店的已安装页面,点击 [参数] 按钮,进入编辑页面修改容器名称。',
|
||||
command: '命令',
|
||||
console: '控制台交互',
|
||||
tty: '伪终端 ( -t )',
|
||||
@ -645,6 +649,8 @@ const message = {
|
||||
emptyUser: '为空时,将使用容器默认的用户登录',
|
||||
privileged: '特权模式',
|
||||
privilegedHelper: '允许容器在主机上执行某些特权操作,可能会增加容器风险,谨慎开启!',
|
||||
editComposeHelper:
|
||||
'注意:设置的环境变量会默认写入 1panel.env 文件。\n如需在容器中使用这些参数,还需在 compose 文件中手动添加 env_file 引用。',
|
||||
|
||||
upgradeHelper: '仓库名称/镜像名称:镜像版本',
|
||||
upgradeWarning2: '升级操作需要重建容器,任何未持久化的数据将会丢失,是否继续?',
|
||||
@ -662,8 +668,8 @@ const message = {
|
||||
containerExample: '80 或者 80-88',
|
||||
exposePort: '暴露端口',
|
||||
exposeAll: '暴露所有',
|
||||
cmdHelper: "例: 'nginx' '-g' 'daemon off;' 或者 nginx -g daemon off;",
|
||||
entrypointHelper: '例: /bin/sh -c',
|
||||
cmdHelper: '例: nginx -g "daemon off;"',
|
||||
entrypointHelper: '例: docker-entrypoint.sh',
|
||||
autoRemove: '容器退出后自动删除容器',
|
||||
cpuQuota: 'CPU 限制',
|
||||
memoryLimit: '内存限制',
|
||||
@ -723,6 +729,7 @@ const message = {
|
||||
urlWarning: '路径前缀不需要添加 http:// 或 https://, 请修改',
|
||||
|
||||
network: '网络',
|
||||
networkHelper: '删除 1panel-network 容器网络将影响部分应用和运行环境的正常使用,是否继续?',
|
||||
createNetwork: '创建网络',
|
||||
networkName: '网络名',
|
||||
driver: '模式',
|
||||
@ -757,11 +764,13 @@ const message = {
|
||||
composePathHelper: '配置文件保存路径: {0}',
|
||||
composeHelper: '通过 1Panel 编辑或者模版创建的编排,将保存在 {0}/docker/compose 路径下',
|
||||
deleteFile: '删除文件',
|
||||
deleteComposeHelper: '删除 {0} 目录下所有文件,包括该文件下的持久化文件等,请谨慎操作!',
|
||||
allDelete: '彻底删除',
|
||||
deleteComposeHelper: '1. 删除容器编排记录 \n2. 删除容器编排的所有文件,包括配置文件和持久化文件',
|
||||
deleteCompose: '" 删除此编排',
|
||||
apps: '应用商店',
|
||||
local: '本地',
|
||||
createCompose: '创建编排',
|
||||
composeDirectory: '编排目录',
|
||||
template: '模版',
|
||||
composeTemplate: '编排模版',
|
||||
createComposeTemplate: '创建编排模版',
|
||||
@ -769,10 +778,14 @@ const message = {
|
||||
content: '内容',
|
||||
contentEmpty: '编排内容不能为空,请输入后重试!',
|
||||
containerNumber: '容器数量',
|
||||
containerStatus: '容器状态',
|
||||
exited: '已停止',
|
||||
running: '运行中',
|
||||
down: '删除',
|
||||
up: '启动',
|
||||
composeDetailHelper: '该 compose 为 1Panel 编排外部创建。暂不支持启停操作。',
|
||||
composeOperatorHelper: '将对 {0} 进行 {1} 操作,是否继续?',
|
||||
composeDownHelper: '将停止并删除 {0} 编排下所有容器及网络,是否继续?',
|
||||
|
||||
setting: '配置',
|
||||
goSetting: '去修改',
|
||||
@ -1366,6 +1379,7 @@ const message = {
|
||||
LOCAL: '服务器磁盘',
|
||||
OSS: '阿里云 OSS',
|
||||
S3: '亚马逊 S3 云存储',
|
||||
mode: '模式',
|
||||
MINIO: 'MINIO',
|
||||
SFTP: 'SFTP',
|
||||
WebDAV: 'WebDAV',
|
||||
@ -1553,6 +1567,7 @@ const message = {
|
||||
allowIPsHelper1: '授权 IP 为空时,则取消授权 IP',
|
||||
allowIPEgs: '当存在多个授权 IP 时,需要换行显示,例: \n172.16.10.111 \n172.16.10.0/24',
|
||||
mfa: '两步验证',
|
||||
mfaClose: '关闭两步验证将导致服务安全性降低,是否继续?',
|
||||
secret: '密钥',
|
||||
mfaAlert: '两步验证密码是基于当前时间生成,请确保服务器时间已同步',
|
||||
mfaHelper: '开启后会验证手机应用验证码',
|
||||
@ -1742,7 +1757,7 @@ const message = {
|
||||
gotoInstalled: '去安装',
|
||||
search: '搜索',
|
||||
limitHelper: '该应用已安装,不支持重复安装',
|
||||
deleteHelper: '{0}已经关联以下资源,无法删除',
|
||||
deleteHelper: '{0}已经关联以下资源,请检查后重试!',
|
||||
checkTitle: '提示',
|
||||
website: '网站',
|
||||
database: '数据库',
|
||||
|
@ -1,12 +1,24 @@
|
||||
<template>
|
||||
<div class="footer">
|
||||
<a href="https://fit2cloud.com/" target="_blank">Copyright © 2014-2024 FIT2CLOUD 飞致云</a>
|
||||
<SystemUpgrade :footer="true" />
|
||||
<div class="footer" :style="{ height: mobile ? '108px' : '48px' }">
|
||||
<div class="flex w-full flex-col gap-4 md:justify-between md:flex-row">
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<a href="https://fit2cloud.com/" target="_blank">Copyright © 2014-2024 {{ $t('commons.fit2cloud') }}</a>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 md:flex-col lg:flex-row">
|
||||
<SystemUpgrade :footer="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import SystemUpgrade from '@/components/system-upgrade/index.vue';
|
||||
import { GlobalStore } from '@/store';
|
||||
const globalStore = GlobalStore();
|
||||
const mobile = computed(() => {
|
||||
return globalStore.isMobile();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -15,8 +27,8 @@ import SystemUpgrade from '@/components/system-upgrade/index.vue';
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 48px;
|
||||
background: #ffffff;
|
||||
border-top: 1px solid #e4e7ed;
|
||||
background: var(--panel-footer-bg);
|
||||
border-top: 1px solid var(--panel-footer-border);
|
||||
box-sizing: border-box;
|
||||
padding: 10px 20px;
|
||||
a {
|
||||
|
@ -177,6 +177,13 @@ function getCheckedLabels(json: Node): string[] {
|
||||
const search = async () => {
|
||||
const res = await getSettingInfo();
|
||||
const json: Node = JSON.parse(res.data.xpackHideMenu);
|
||||
if (json.isCheck === false) {
|
||||
json.children.forEach((child: any) => {
|
||||
if (child.isCheck === true) {
|
||||
child.isCheck = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
const checkedLabels = getCheckedLabels(json);
|
||||
let rstMenuList: RouteRecordRaw[] = [];
|
||||
menuStore.menuList.forEach((item) => {
|
||||
|
@ -101,6 +101,7 @@ onMounted(() => {
|
||||
loadStatus();
|
||||
loadProductProFromDB();
|
||||
loadMasterProductProFromDB();
|
||||
globalStore.isFullScreen = false;
|
||||
|
||||
const mqList = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
if (mqList.addEventListener) {
|
||||
|
@ -34,7 +34,7 @@ const settingRouter = {
|
||||
hidden: true,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
activeMenu: 'Setting',
|
||||
activeMenu: '/settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -44,7 +44,7 @@ const settingRouter = {
|
||||
hidden: true,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
activeMenu: 'Setting',
|
||||
activeMenu: '/settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -54,7 +54,7 @@ const settingRouter = {
|
||||
hidden: true,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
activeMenu: 'Setting',
|
||||
activeMenu: '/settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -64,7 +64,7 @@ const settingRouter = {
|
||||
hidden: true,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
activeMenu: 'Setting',
|
||||
activeMenu: '/settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -74,7 +74,7 @@ const settingRouter = {
|
||||
component: () => import('@/views/setting/snapshot/index.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
activeMenu: 'Setting',
|
||||
activeMenu: '/settings',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -182,7 +182,7 @@ html {
|
||||
|
||||
.mask-prompt {
|
||||
position: absolute;
|
||||
z-index: 9998;
|
||||
z-index: 1;
|
||||
top: 220px;
|
||||
left: 45%;
|
||||
transform: translate(-50%, -50%);
|
||||
@ -407,6 +407,10 @@ html {
|
||||
width: 200px !important;
|
||||
}
|
||||
|
||||
.p-w-250 {
|
||||
width: 250px !important;
|
||||
}
|
||||
|
||||
.p-w-100 {
|
||||
width: 100px !important;
|
||||
}
|
||||
@ -431,6 +435,12 @@ html {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dialog-footer{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.monaco-editor-tree-light .el-tree-node__content:hover {
|
||||
background-color: #e5eefd;
|
||||
}
|
||||
|
@ -390,6 +390,19 @@ export function checkCidr(value: string): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export function checkCidrV6(value: string): boolean {
|
||||
if (value === '') {
|
||||
return true;
|
||||
}
|
||||
if (checkIpV6(value.split('/')[0])) {
|
||||
return true;
|
||||
}
|
||||
const reg = /^(?:[1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$/;
|
||||
if (!reg.test(value.split('/')[1])) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function checkPort(value: string): boolean {
|
||||
if (Number(value) <= 0) {
|
||||
|
@ -45,6 +45,11 @@
|
||||
></CodemirrorPro>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('container.env')" prop="envStr">
|
||||
<el-input type="textarea" :placeholder="$t('container.tagHelper')" :rows="3" v-model="form.envStr" />
|
||||
</el-form-item>
|
||||
<span class="input-help">{{ $t('container.editComposeHelper') }}</span>
|
||||
<CodemirrorPro v-model="form.envFileContent" :height="45" :minHeight="45" disabled mode="yaml" />
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
@ -88,6 +93,9 @@ const form = reactive({
|
||||
path: '',
|
||||
file: '',
|
||||
template: null as number,
|
||||
env: [],
|
||||
envStr: '',
|
||||
envFileContent: `env_file:\n - 1panel.env`,
|
||||
});
|
||||
const rules = reactive({
|
||||
name: [Rules.requiredInput, Rules.imageName],
|
||||
@ -107,6 +115,8 @@ const acceptParams = (): void => {
|
||||
form.path = '';
|
||||
form.file = '';
|
||||
form.template = null;
|
||||
form.env = [];
|
||||
form.envStr = '';
|
||||
loadTemplates();
|
||||
loadPath();
|
||||
};
|
||||
@ -180,6 +190,9 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
MsgError(i18n.global.t('container.contentEmpty'));
|
||||
return;
|
||||
}
|
||||
if (form.envStr) {
|
||||
form.env = form.envStr.split('\n');
|
||||
}
|
||||
loading.value = true;
|
||||
await testCompose(form)
|
||||
.then(async (res) => {
|
||||
|
@ -7,9 +7,9 @@
|
||||
>
|
||||
<el-form ref="deleteForm" v-loading="loading">
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="deleteFile" :label="$t('container.deleteFile')" />
|
||||
<span class="input-help">
|
||||
{{ $t('container.deleteComposeHelper', [loadComposeDir()]) }}
|
||||
<el-checkbox v-model="deleteFile" :label="$t('container.allDelete')" />
|
||||
<span class="input-help whitespace-break-spaces">
|
||||
{{ $t('container.deleteComposeHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
@ -64,21 +64,12 @@ const acceptParams = async (prop: DialogProps) => {
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const loadComposeDir = () => {
|
||||
const parts = composePath.value.split('/');
|
||||
if (parts.length <= 2) {
|
||||
return '/';
|
||||
}
|
||||
const parentDirectory = parts.slice(0, -1).join('/');
|
||||
return parentDirectory;
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
loading.value = true;
|
||||
let params = {
|
||||
name: composeName.value,
|
||||
path: composePath.value,
|
||||
operation: 'down',
|
||||
operation: 'delete',
|
||||
withFile: deleteFile.value,
|
||||
};
|
||||
await composeOperator(params)
|
||||
|
@ -7,7 +7,7 @@
|
||||
<el-tag effect="dark" type="success">{{ composeName }}</el-tag>
|
||||
</div>
|
||||
<div v-if="createdBy === '1Panel'" style="margin-left: 50px">
|
||||
<el-button link type="primary" @click="onComposeOperate('start')">
|
||||
<el-button link type="primary" @click="onComposeOperate('up')">
|
||||
{{ $t('container.start') }}
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
@ -143,7 +143,6 @@ const dialogContainerLogRef = ref();
|
||||
|
||||
const opRef = ref();
|
||||
|
||||
const emit = defineEmits<{ (e: 'back'): void }>();
|
||||
interface DialogProps {
|
||||
createdBy: string;
|
||||
name: string;
|
||||
@ -177,7 +176,7 @@ const search = async () => {
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
filters: filterItem,
|
||||
orderBy: 'created_at',
|
||||
orderBy: 'createdAt',
|
||||
order: 'null',
|
||||
};
|
||||
loading.value = true;
|
||||
@ -260,15 +259,18 @@ const onOperate = async (op: string) => {
|
||||
};
|
||||
|
||||
const onComposeOperate = async (operation: string) => {
|
||||
ElMessageBox.confirm(
|
||||
i18n.global.t('container.composeOperatorHelper', [composeName.value, i18n.global.t('container.' + operation)]),
|
||||
i18n.global.t('container.' + operation),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
},
|
||||
).then(async () => {
|
||||
let mes =
|
||||
operation === 'down'
|
||||
? i18n.global.t('container.composeDownHelper', [composeName.value])
|
||||
: i18n.global.t('container.composeOperatorHelper', [
|
||||
composeName.value,
|
||||
i18n.global.t('container.' + operation),
|
||||
]);
|
||||
ElMessageBox.confirm(mes, i18n.global.t('container.' + operation), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
}).then(async () => {
|
||||
let params = {
|
||||
name: composeName.value,
|
||||
path: composePath.value,
|
||||
@ -280,11 +282,7 @@ const onComposeOperate = async (operation: string) => {
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
if (operation === 'down') {
|
||||
emit('back');
|
||||
} else {
|
||||
search();
|
||||
}
|
||||
search();
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
|
@ -7,11 +7,36 @@
|
||||
size="large"
|
||||
>
|
||||
<div v-loading="loading">
|
||||
<CodemirrorPro
|
||||
v-model="content"
|
||||
mode="yaml"
|
||||
placeholder="#Define or paste the content of your docker-compose file here"
|
||||
></CodemirrorPro>
|
||||
<el-form ref="formRef" @submit.prevent label-position="top">
|
||||
<el-form-item>
|
||||
<CodemirrorPro
|
||||
v-model="content"
|
||||
mode="yaml"
|
||||
:heightDiff="225"
|
||||
placeholder="#Define or paste the content of your docker-compose file here"
|
||||
></CodemirrorPro>
|
||||
</el-form-item>
|
||||
<div v-if="createdBy === '1Panel'">
|
||||
<el-form-item :label="$t('container.env')" prop="environmentStr">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:placeholder="$t('container.tagHelper')"
|
||||
:rows="3"
|
||||
v-model="environmentStr"
|
||||
/>
|
||||
</el-form-item>
|
||||
<span class="input-help whitespace-break-spaces">
|
||||
{{ $t('container.editComposeHelper') }}
|
||||
</span>
|
||||
<CodemirrorPro
|
||||
v-model="envFileContent"
|
||||
:height="45"
|
||||
:minHeight="45"
|
||||
disabled
|
||||
mode="yaml"
|
||||
></CodemirrorPro>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
@ -36,13 +61,25 @@ const composeVisible = ref(false);
|
||||
const path = ref();
|
||||
const content = ref();
|
||||
const name = ref();
|
||||
const environmentStr = ref();
|
||||
const environmentEnv = ref();
|
||||
const createdBy = ref();
|
||||
const envFileContent = ref(`env_file:\n - 1panel.env`);
|
||||
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
const onSubmitEdit = async () => {
|
||||
const param = {
|
||||
name: name.value,
|
||||
path: path.value,
|
||||
content: content.value,
|
||||
env: environmentStr.value,
|
||||
createdBy: createdBy.value,
|
||||
};
|
||||
if (environmentStr.value != undefined) {
|
||||
param.env = environmentStr.value.split('\n');
|
||||
emit('search');
|
||||
}
|
||||
loading.value = true;
|
||||
await composeUpdate(param)
|
||||
.then(() => {
|
||||
@ -59,13 +96,18 @@ interface DialogProps {
|
||||
name: string;
|
||||
path: string;
|
||||
content: string;
|
||||
env: Array<string>;
|
||||
envStr: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
const acceptParams = (props: DialogProps): void => {
|
||||
composeVisible.value = true;
|
||||
path.value = props.path;
|
||||
name.value = props.name;
|
||||
content.value = props.content;
|
||||
createdBy.value = props.createdBy;
|
||||
environmentEnv.value = props.env || [];
|
||||
environmentStr.value = environmentEnv.value.join('\n');
|
||||
};
|
||||
const handleClose = () => {
|
||||
composeVisible.value = false;
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<div v-show="isOnDetail">
|
||||
<ComposeDetail @back="backList" ref="composeDetailRef" />
|
||||
<ComposeDetail ref="composeDetailRef" />
|
||||
</div>
|
||||
<el-card v-if="dockerStatus != 'Running'" class="mask-prompt">
|
||||
<span>{{ $t('container.serviceUnavailable') }}</span>
|
||||
@ -10,20 +10,6 @@
|
||||
</el-card>
|
||||
|
||||
<LayoutContent v-if="!isOnDetail" :title="$t('container.compose')" :class="{ mask: dockerStatus != 'Running' }">
|
||||
<template #prompt>
|
||||
<el-alert type="info" :closable="false">
|
||||
<template #title>
|
||||
<span class="flx-align-center">
|
||||
<span>{{ $t('container.composeHelper', [baseDir]) }}</span>
|
||||
<el-button type="primary" link @click="toFolder">
|
||||
<el-icon>
|
||||
<FolderOpened />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-alert>
|
||||
</template>
|
||||
<template #leftToolBar>
|
||||
<el-button type="primary" @click="onOpenDialog()">
|
||||
{{ $t('container.createCompose') }}
|
||||
@ -46,6 +32,7 @@
|
||||
:label="$t('commons.table.name')"
|
||||
width="170"
|
||||
prop="name"
|
||||
sortable
|
||||
fix
|
||||
show-overflow-tooltip
|
||||
>
|
||||
@ -62,6 +49,22 @@
|
||||
<span v-if="row.createdBy === '1Panel'">1Panel</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('container.composeDirectory')" min-width="80" fix>
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="toComposeFolder(row)">
|
||||
<el-icon>
|
||||
<FolderOpened />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('container.containerStatus')" min-width="80" fix>
|
||||
<template #default="scope">
|
||||
<div>
|
||||
{{ getContainerStatus(scope.row.containers) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('container.containerNumber')"
|
||||
prop="containerNumber"
|
||||
@ -95,7 +98,6 @@ import ComposeDetail from '@/views/container/compose/detail/index.vue';
|
||||
import { loadContainerLog, loadDockerStatus, searchCompose } from '@/api/modules/container';
|
||||
import i18n from '@/lang';
|
||||
import { Container } from '@/api/interface/container';
|
||||
import { loadBaseDir } from '@/api/modules/setting';
|
||||
import router from '@/routers';
|
||||
|
||||
const data = ref();
|
||||
@ -103,7 +105,6 @@ const selects = ref<any>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const isOnDetail = ref(false);
|
||||
const baseDir = ref();
|
||||
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'container-compose-page-size',
|
||||
@ -133,13 +134,8 @@ const goSetting = async () => {
|
||||
router.push({ name: 'ContainerSetting' });
|
||||
};
|
||||
|
||||
const toFolder = async () => {
|
||||
router.push({ path: '/hosts/files', query: { path: baseDir.value + '/docker/compose' } });
|
||||
};
|
||||
|
||||
const loadPath = async () => {
|
||||
const pathRes = await loadBaseDir();
|
||||
baseDir.value = pathRes.data;
|
||||
const toComposeFolder = async (row: Container.ComposeInfo) => {
|
||||
router.push({ path: '/hosts/files', query: { path: row.workdir } });
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
@ -171,9 +167,17 @@ const loadDetail = async (row: Container.ComposeInfo) => {
|
||||
isOnDetail.value = true;
|
||||
composeDetailRef.value!.acceptParams(params);
|
||||
};
|
||||
const backList = async () => {
|
||||
isOnDetail.value = false;
|
||||
search();
|
||||
|
||||
const getContainerStatus = (containers) => {
|
||||
const safeContainers = containers || [];
|
||||
const runningCount = safeContainers.filter((container) => container.state.toLowerCase() === 'running').length;
|
||||
const totalCount = safeContainers.length;
|
||||
const statusText = runningCount > 0 ? 'Running' : 'Exited';
|
||||
if (statusText === 'Exited') {
|
||||
return i18n.global.t('container.exited');
|
||||
} else {
|
||||
return i18n.global.t('container.running') + ` (${runningCount}/${totalCount})`;
|
||||
}
|
||||
};
|
||||
|
||||
const dialogRef = ref();
|
||||
@ -222,7 +226,6 @@ const buttons = [
|
||||
},
|
||||
];
|
||||
onMounted(() => {
|
||||
loadPath();
|
||||
loadStatus();
|
||||
});
|
||||
</script>
|
||||
|
@ -77,7 +77,7 @@
|
||||
<el-button type="primary" plain @click="onClean()">
|
||||
{{ $t('container.containerPrune') }}
|
||||
</el-button>
|
||||
<el-button-group class="ml-4">
|
||||
<el-button-group>
|
||||
<el-button :disabled="checkStatus('start', null)" @click="onOperate('start', null)">
|
||||
{{ $t('container.start') }}
|
||||
</el-button>
|
||||
@ -102,12 +102,12 @@
|
||||
</el-button-group>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<el-checkbox v-model="includeAppStore" @change="search()" class="!mr-2.5">
|
||||
<el-checkbox v-model="includeAppStore" @change="search()">
|
||||
{{ $t('container.includeAppstore') }}
|
||||
</el-checkbox>
|
||||
<TableSearch @search="search()" v-model:searchName="searchName" class="mr-2.5" />
|
||||
<TableRefresh @search="search()" class="mr-2.5" />
|
||||
<TableSetting title="container-refresh" @search="refresh()" class="mr-2.5" />
|
||||
<TableSearch @search="search()" v-model:searchName="searchName" />
|
||||
<TableRefresh @search="search()" />
|
||||
<TableSetting title="container-refresh" @search="refresh()" />
|
||||
<fu-table-column-select
|
||||
:columns="columns"
|
||||
trigger="hover"
|
||||
@ -444,7 +444,7 @@ const paginationConfig = reactive({
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
state: 'all',
|
||||
orderBy: 'created_at',
|
||||
orderBy: 'createdAt',
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
@ -452,7 +452,7 @@ const dialogUpgradeRef = ref();
|
||||
const dialogCommitRef = ref();
|
||||
const dialogPortJumpRef = ref();
|
||||
const opRef = ref();
|
||||
const includeAppStore = ref(true);
|
||||
const includeAppStore = ref();
|
||||
const columns = ref([]);
|
||||
|
||||
const countItem = reactive({
|
||||
|
@ -96,17 +96,16 @@ const timeOptions = ref([
|
||||
]);
|
||||
|
||||
function toggleFullscreen() {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.toggle();
|
||||
}
|
||||
globalStore.isFullScreen = !globalStore.isFullScreen;
|
||||
}
|
||||
|
||||
const loadTooltip = () => {
|
||||
return i18n.global.t('commons.button.' + (screenfull.isFullscreen ? 'quitFullscreen' : 'fullscreen'));
|
||||
return i18n.global.t('commons.button.' + (globalStore.isFullScreen ? 'quitFullscreen' : 'fullscreen'));
|
||||
};
|
||||
const handleClose = async () => {
|
||||
terminalSocket.value?.send('close conn');
|
||||
logVisible.value = false;
|
||||
globalStore.isFullScreen = false;
|
||||
};
|
||||
watch(logVisible, (val) => {
|
||||
if (screenfull.isEnabled && !val && !mobile.value) screenfull.exit();
|
||||
@ -131,10 +130,8 @@ const searchLogs = async () => {
|
||||
};
|
||||
|
||||
const onDownload = async () => {
|
||||
let msg =
|
||||
logSearch.tail === 0
|
||||
? i18n.global.t('container.downLogHelper1', [logSearch.container])
|
||||
: i18n.global.t('container.downLogHelper2', [logSearch.container, logSearch.tail]);
|
||||
logSearch.tail = 0;
|
||||
let msg = i18n.global.t('container.downLogHelper1', [logSearch.container]);
|
||||
ElMessageBox.confirm(msg, i18n.global.t('file.download'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
|
55
frontend/src/views/container/container/operate/confirm.vue
Normal file
55
frontend/src/views/container/container/operate/confirm.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogVisible" width="30%" :title="$t('commons.button.edit')">
|
||||
<div v-if="isFromApp" class="leading-6">
|
||||
<div>
|
||||
<span>{{ $t('container.updateHelper1') }}</span>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<span>{{ $t('container.updateHelper2') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ $t('container.updateHelper3') }}</span>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ $t('container.updateHelper4') }}</span>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button :disabled="loading" @click="dialogVisible = false">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button :disabled="loading" type="primary" @click="onSubmit()">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const loading = ref();
|
||||
const dialogVisible = ref(false);
|
||||
const isFromApp = ref();
|
||||
|
||||
interface DialogProps {
|
||||
isFromApp: boolean;
|
||||
}
|
||||
|
||||
const acceptParams = (props: DialogProps): void => {
|
||||
isFromApp.value = props.isFromApp;
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
const emit = defineEmits(['submit']);
|
||||
|
||||
const onSubmit = async () => {
|
||||
emit('submit');
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
@ -424,6 +424,7 @@
|
||||
</template>
|
||||
</LayoutContent>
|
||||
<Command ref="commandRef" />
|
||||
<Confirm ref="confirmRef" @submit="submit" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -431,8 +432,9 @@
|
||||
import { reactive, ref } from 'vue';
|
||||
import { Rules, checkFloatNumberRange, checkNumberRange } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm, ElMessageBox } from 'element-plus';
|
||||
import { ElForm } from 'element-plus';
|
||||
import Command from '@/views/container/container/command/index.vue';
|
||||
import Confirm from '@/views/container/container/operate/confirm.vue';
|
||||
import {
|
||||
listImage,
|
||||
listVolume,
|
||||
@ -450,6 +452,7 @@ import router from '@/routers';
|
||||
|
||||
const loading = ref(false);
|
||||
const isCreate = ref();
|
||||
const confirmRef = ref();
|
||||
const form = reactive<Container.ContainerHelper>({
|
||||
containerID: '',
|
||||
name: '',
|
||||
@ -507,23 +510,31 @@ const search = async () => {
|
||||
form.autoRemove = res.data.autoRemove;
|
||||
form.restartPolicy = res.data.restartPolicy;
|
||||
form.memory = Number(res.data.memory.toFixed(2));
|
||||
form.cmd = res.data.cmd || [];
|
||||
form.user = res.data.user;
|
||||
form.workingDir = res.data.workingDir;
|
||||
let itemCmd = '';
|
||||
for (const item of form.cmd) {
|
||||
itemCmd += `'${item}' `;
|
||||
}
|
||||
form.cmdStr = itemCmd ? itemCmd.substring(0, itemCmd.length - 1) : '';
|
||||
|
||||
let itemEntrypoint = '';
|
||||
if (res.data.entrypoint) {
|
||||
for (const item of res.data.entrypoint) {
|
||||
itemEntrypoint += `'${item}' `;
|
||||
let itemCmd = '';
|
||||
form.cmd = res.data.cmd || [];
|
||||
for (const item of form.cmd) {
|
||||
if (item.indexOf(' ') !== -1) {
|
||||
itemCmd += `"${item.replaceAll('"', '\\"')}" `;
|
||||
} else {
|
||||
itemCmd += item + ' ';
|
||||
}
|
||||
}
|
||||
form.cmdStr = itemCmd.trimEnd();
|
||||
|
||||
let itemEntrypoint = '';
|
||||
form.entrypoint = res.data.entrypoint || [];
|
||||
for (const item of form.entrypoint) {
|
||||
if (item.indexOf(' ') !== -1) {
|
||||
itemEntrypoint += `"${item.replaceAll('"', '\\"')}" `;
|
||||
} else {
|
||||
itemEntrypoint += item + ' ';
|
||||
}
|
||||
}
|
||||
form.entrypointStr = itemEntrypoint.trimEnd();
|
||||
|
||||
form.entrypointStr = itemEntrypoint ? itemEntrypoint.substring(0, itemEntrypoint.length - 1) : '';
|
||||
form.labels = res.data.labels || [];
|
||||
form.env = res.data.env || [];
|
||||
form.exposedPorts = res.data.exposedPorts || [];
|
||||
@ -635,84 +646,62 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
form.cmd = [];
|
||||
if (form.cmdStr) {
|
||||
if (form.cmdStr.indexOf(`'`) !== -1) {
|
||||
let itemCmd = form.cmdStr.split(`'`);
|
||||
for (const cmd of itemCmd) {
|
||||
if (cmd && cmd !== ' ') {
|
||||
form.cmd.push(cmd);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let itemCmd = form.cmdStr.split(` `);
|
||||
for (const cmd of itemCmd) {
|
||||
form.cmd.push(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
form.entrypoint = [];
|
||||
if (form.entrypointStr) {
|
||||
if (form.entrypointStr.indexOf(`'`) !== -1) {
|
||||
let itemEntrypoint = form.entrypointStr.split(`'`);
|
||||
for (const entry of itemEntrypoint) {
|
||||
if (entry && entry !== ' ') {
|
||||
form.entrypoint.push(entry);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let itemEntrypoint = form.entrypointStr.split(` `);
|
||||
for (const entry of itemEntrypoint) {
|
||||
form.entrypoint.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (form.publishAllPorts) {
|
||||
form.exposedPorts = [];
|
||||
} else {
|
||||
if (!checkPortValid()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
form.memory = Number(form.memory);
|
||||
form.nanoCPUs = Number(form.nanoCPUs);
|
||||
|
||||
loading.value = true;
|
||||
if (isCreate.value) {
|
||||
await createContainer(form)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
submit();
|
||||
} else {
|
||||
ElMessageBox.confirm(
|
||||
i18n.global.t('container.updateContainerHelper'),
|
||||
i18n.global.t('commons.button.edit'),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
},
|
||||
)
|
||||
.then(async () => {
|
||||
await updateContainer(form)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
})
|
||||
.catch(() => {
|
||||
updateContainerID();
|
||||
loading.value = false;
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
confirmRef.value.acceptParams({ isFromApp: isFromApp(form) });
|
||||
}
|
||||
});
|
||||
};
|
||||
const submit = async () => {
|
||||
form.cmd = [];
|
||||
if (form.cmdStr) {
|
||||
let itemCmd = splitWithQuotes(form.cmdStr);
|
||||
for (const item of itemCmd) {
|
||||
form.cmd.push(item.replace(/(?<!\\)"/g, '').replaceAll('\\"', '"'));
|
||||
}
|
||||
}
|
||||
form.entrypoint = [];
|
||||
if (form.entrypointStr) {
|
||||
let itemEntrypoint = splitWithQuotes(form.entrypointStr);
|
||||
for (const item of itemEntrypoint) {
|
||||
form.entrypoint.push(item.replace(/(?<!\\)"/g, '').replaceAll('\\"', '"'));
|
||||
}
|
||||
}
|
||||
if (form.publishAllPorts) {
|
||||
form.exposedPorts = [];
|
||||
} else {
|
||||
if (!checkPortValid()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
form.memory = Number(form.memory);
|
||||
form.nanoCPUs = Number(form.nanoCPUs);
|
||||
|
||||
loading.value = true;
|
||||
if (isCreate.value) {
|
||||
await createContainer(form)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
goBack();
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
} else {
|
||||
await updateContainer(form)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
goBack();
|
||||
})
|
||||
.catch(() => {
|
||||
updateContainerID();
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateContainerID = async () => {
|
||||
let params = {
|
||||
@ -721,7 +710,7 @@ const updateContainerID = async () => {
|
||||
state: 'all',
|
||||
name: form.name,
|
||||
filters: '',
|
||||
orderBy: 'created_at',
|
||||
orderBy: 'createdAt',
|
||||
order: 'null',
|
||||
};
|
||||
await searchContainer(params).then((res) => {
|
||||
@ -788,6 +777,18 @@ const isFromApp = (rowData: Container.ContainerHelper) => {
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const splitWithQuotes = (str) => {
|
||||
str = str.replace(/\\"/g, '<quota>');
|
||||
const regex = /(?=(?:[^'"]|['"][^'"]*['"])*$)\s+/g;
|
||||
let parts = str.split(regex).filter(Boolean);
|
||||
let returnList = [];
|
||||
for (const item of parts) {
|
||||
returnList.push(item.replaceAll('<quota>', '\\"'));
|
||||
}
|
||||
return returnList;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (router.currentRoute.value.query.containerID) {
|
||||
isCreate.value = false;
|
||||
|
@ -196,20 +196,29 @@ const search = async () => {
|
||||
|
||||
const batchDelete = async (row: Container.NetworkInfo | null) => {
|
||||
let names: Array<string> = [];
|
||||
let hasPanelNetwork;
|
||||
if (row === null) {
|
||||
selects.value.forEach((item: Container.NetworkInfo) => {
|
||||
if (item.name === '1panel-network') {
|
||||
hasPanelNetwork = true;
|
||||
}
|
||||
names.push(item.name);
|
||||
});
|
||||
} else {
|
||||
if (row.name === '1panel-network') {
|
||||
hasPanelNetwork = true;
|
||||
}
|
||||
names.push(row.name);
|
||||
}
|
||||
opRef.value.acceptParams({
|
||||
title: i18n.global.t('commons.button.delete'),
|
||||
names: names,
|
||||
msg: i18n.global.t('commons.msg.operatorHelper', [
|
||||
i18n.global.t('container.network'),
|
||||
i18n.global.t('commons.button.delete'),
|
||||
]),
|
||||
msg: hasPanelNetwork
|
||||
? i18n.global.t('container.networkHelper')
|
||||
: i18n.global.t('commons.msg.operatorHelper', [
|
||||
i18n.global.t('container.network'),
|
||||
i18n.global.t('commons.button.delete'),
|
||||
]),
|
||||
api: deleteNetwork,
|
||||
params: { names: names },
|
||||
});
|
||||
|
@ -44,7 +44,13 @@
|
||||
<el-row class="p-mt-20" v-if="confShowType === 'base'">
|
||||
<el-col :span="1"><br /></el-col>
|
||||
<el-col :xs="24" :sm="24" :md="15" :lg="12" :xl="10">
|
||||
<el-form :model="form" label-position="left" :rules="rules" ref="formRef" label-width="120px">
|
||||
<el-form
|
||||
:model="form"
|
||||
:label-position="mobile ? 'top' : 'left'"
|
||||
:rules="rules"
|
||||
ref="formRef"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item :label="$t('container.mirrors')" prop="mirrors">
|
||||
<div class="w-full" v-if="form.mirrors">
|
||||
<el-input
|
||||
@ -230,7 +236,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElMessageBox, FormInstance } from 'element-plus';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { onMounted, reactive, ref, computed } from 'vue';
|
||||
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
|
||||
import Mirror from '@/views/container/setting/mirror/index.vue';
|
||||
import Registry from '@/views/container/setting/registry/index.vue';
|
||||
@ -252,6 +258,9 @@ import { checkNumberRange } from '@/global/form-rules';
|
||||
import { GlobalStore } from '@/store';
|
||||
const globalStore = GlobalStore();
|
||||
|
||||
const mobile = computed(() => {
|
||||
return globalStore.isMobile();
|
||||
});
|
||||
const unset = ref(i18n.global.t('setting.unSetting'));
|
||||
const submitInput = ref();
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<DrawerPro v-model="detailVisible" :header="$t('commons.button.view')" size="large">
|
||||
<DrawerPro v-model="detailVisible" :header="$t('commons.button.view')" :back="handleClose" size="large">
|
||||
<CodemirrorPro
|
||||
:placeholder="$t('commons.msg.noneData')"
|
||||
v-model="detailInfo"
|
||||
@ -29,6 +29,10 @@ const acceptParams = (params: DialogProps): void => {
|
||||
detailVisible.value = true;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
detailVisible.value = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
|
@ -33,6 +33,7 @@
|
||||
:label="$t('commons.table.name')"
|
||||
min-width="100"
|
||||
prop="name"
|
||||
sortable
|
||||
fix
|
||||
show-overflow-tooltip
|
||||
>
|
||||
@ -45,7 +46,7 @@
|
||||
<el-table-column :label="$t('container.description')" prop="description" min-width="200" fix />
|
||||
<el-table-column :label="$t('commons.table.createdAt')" min-width="80" fix>
|
||||
<template #default="{ row }">
|
||||
{{ dateFormatSimple(row.createdAt) }}
|
||||
{{ dateFormat(0, 0, row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" />
|
||||
@ -61,7 +62,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, onMounted, ref } from 'vue';
|
||||
import { dateFormatSimple } from '@/utils/util';
|
||||
import { dateFormat } from '@/utils/util';
|
||||
import { Container } from '@/api/interface/container';
|
||||
import DetailDialog from '@/views/container/template/detail/index.vue';
|
||||
import OperatorDialog from '@/views/container/template/operator/index.vue';
|
||||
|
@ -61,7 +61,7 @@
|
||||
icon="VideoPlay"
|
||||
type="success"
|
||||
>
|
||||
{{ $t('commons.status.enabled') }}
|
||||
{{ $t('commons.button.enable') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
@ -70,7 +70,7 @@
|
||||
type="danger"
|
||||
@click="onChangeStatus(row.id, 'enable')"
|
||||
>
|
||||
{{ $t('commons.status.disabled') }}
|
||||
{{ $t('commons.button.disable') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@ -201,7 +201,7 @@ const paginationConfig = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
orderBy: 'created_at',
|
||||
orderBy: 'createdAt',
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
|
@ -742,6 +742,7 @@ const rules = reactive({
|
||||
],
|
||||
|
||||
script: [{ validator: verifyScript, trigger: 'blur', required: true }],
|
||||
appID: [Rules.requiredSelect],
|
||||
website: [Rules.requiredSelect],
|
||||
dbName: [Rules.requiredSelect],
|
||||
url: [Rules.requiredInput],
|
||||
|
@ -61,14 +61,19 @@
|
||||
<span class="input-help">{{ $t('database.remoteConnHelper') }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('database.rootPassword')" :rules="Rules.paramComplexity" prop="password">
|
||||
<el-input type="password" show-password clearable v-model="form.password">
|
||||
<template #append>
|
||||
<CopyButton :content="form.password" />
|
||||
<el-button @click="random" class="p-ml-5">
|
||||
{{ $t('commons.button.random') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-input
|
||||
style="width: calc(100% - 147px)"
|
||||
type="password"
|
||||
show-password
|
||||
clearable
|
||||
v-model="form.password"
|
||||
/>
|
||||
<el-button-group>
|
||||
<CopyButton class="copy_button" :content="form.password" />
|
||||
<el-button @click="random">
|
||||
{{ $t('commons.button.random') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div v-if="form.from !== 'local'">
|
||||
@ -263,3 +268,14 @@ defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.copy_button {
|
||||
border-radius: 0px;
|
||||
border-left-width: 0px;
|
||||
}
|
||||
:deep(.el-input__wrapper) {
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
</style>
|
||||
|
@ -17,8 +17,9 @@
|
||||
:app-name="appName"
|
||||
v-model:loading="loading"
|
||||
v-model:mask-show="maskShow"
|
||||
@setting="onSetting"
|
||||
@setting="onSetting()"
|
||||
@is-exist="checkExist"
|
||||
ref="appStatusRef"
|
||||
></AppStatus>
|
||||
</template>
|
||||
<template #leftToolBar>
|
||||
@ -29,7 +30,7 @@
|
||||
>
|
||||
{{ $t('database.create') }}
|
||||
</el-button>
|
||||
<el-button v-if="currentDB" @click="onChangeConn" type="primary" plain>
|
||||
<el-button v-if="currentDB" @click="onChangeConn()" type="primary" plain>
|
||||
{{ $t('database.databaseConnInfo') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
@ -40,10 +41,10 @@
|
||||
>
|
||||
{{ $t('database.loadFromRemote') }}
|
||||
</el-button>
|
||||
<el-button @click="goRemoteDB" type="primary" plain>
|
||||
<el-button @click="goRemoteDB()" type="primary" plain>
|
||||
{{ $t('database.remoteDB') }}
|
||||
</el-button>
|
||||
<el-dropdown class="ml-3">
|
||||
<el-dropdown>
|
||||
<el-button type="primary" plain>
|
||||
{{ $t('database.manage') }}
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
@ -61,7 +62,7 @@
|
||||
</el-dropdown>
|
||||
</template>
|
||||
<template #rightToolBar>
|
||||
<el-select v-model="currentDBName" @change="changeDatabase()" class="p-w-200 mr-2.5" v-if="currentDB">
|
||||
<el-select v-model="currentDBName" @change="changeDatabase()" class="p-w-250" v-if="currentDB">
|
||||
<template #prefix>{{ $t('commons.table.type') }}</template>
|
||||
<el-option-group :label="$t('database.local')">
|
||||
<div v-for="(item, index) in dbOptionsLocal" :key="index">
|
||||
@ -314,6 +315,8 @@ const dashboardName = ref();
|
||||
const dashboardKey = ref();
|
||||
const dashboardVisible = ref(false);
|
||||
|
||||
const appStatusRef = ref();
|
||||
|
||||
const dialogPortJumpRef = ref();
|
||||
|
||||
const data = ref();
|
||||
@ -322,7 +325,7 @@ const paginationConfig = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: Number(localStorage.getItem('mysql-page-size')) || 10,
|
||||
total: 0,
|
||||
orderBy: 'created_at',
|
||||
orderBy: 'createdAt',
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
@ -377,6 +380,7 @@ const changeDatabase = async () => {
|
||||
appKey.value = item.type;
|
||||
appName.value = item.database;
|
||||
search();
|
||||
appStatusRef.value?.onCheck(appKey.value, appName.value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user