1
0
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:
ssongliu 2024-12-17 15:59:21 +08:00 committed by GitHub
parent a8170a499a
commit a627aa9915
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
134 changed files with 1624 additions and 1479 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,4 +12,5 @@ type Compose struct {
BaseModel
Name string `json:"name"`
Path string `json:"path"`
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -128,7 +128,8 @@ var (
)
var (
ErrFirewall = "ErrFirewall"
ErrFirewallNone = "ErrFirewallNone"
ErrFirewallBoth = "ErrFirewallBoth"
)
var (

View File

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

View File

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

View File

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

View File

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

View File

@ -182,7 +182,8 @@ ErrConfigAlreadyExist: "已存在同名配置文件"
ErrUserFindErr: "用戶 {{ .name }} 查找失敗 {{ .err }}"
#ssh
ErrFirewall: "當前未檢測到系統 firewalld 或 ufw 服務,請檢查後重試!"
ErrFirewallNone: "未檢測到系統 firewalld 或 ufw 服務,請檢查後重試!"
ErrFirewallBoth: "檢測到系統同時存在 firewalld 或 ufw 服務,為避免衝突,請卸載後重試!"
#cronjob
ErrCutWebsiteLog: "{{ .name }} 網站日誌切割失敗,錯誤 {{ .err }}"

View File

@ -180,7 +180,8 @@ ErrConfigAlreadyExist: "已存在同名配置文件"
ErrUserFindErr: "用户 {{ .name }} 查找失败 {{ .err }}"
#ssh
ErrFirewall: "当前未检测到系统 firewalld 或 ufw 服务,请检查后重试!"
ErrFirewallNone: "未检测到系统 firewalld 或 ufw 服务,请检查后重试!"
ErrFirewallBoth: "检测到系统同时存在 firewalld 或 ufw 服务,为避免冲突,请卸载后重试!"
#cronjob
ErrCutWebsiteLog: "{{ .name }} 网站日志切割失败,错误 {{ .err }}"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
/usr/songliu/xpack-backend/other/entry_xpack.go

View File

@ -0,0 +1 @@
/usr/songliu/xpack-backend/other/init_xpack.go

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ export namespace Backup {
credential: string;
rememberAuth: boolean;
backupPath: string;
bucketInput: boolean;
vars: string;
varsJson: object;
createdAt: Date;

View File

@ -268,6 +268,8 @@ export namespace Container {
path: string;
containers: Array<ComposeContainer>;
expand: boolean;
envStr: string;
env: Array<string>;
}
export interface ComposeContainer {
name: string;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: '數據庫',

View File

@ -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: '数据库',

View File

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

View File

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

View File

@ -101,6 +101,7 @@ onMounted(() => {
loadStatus();
loadProductProFromDB();
loadMasterProductProFromDB();
globalStore.isFullScreen = false;
const mqList = window.matchMedia('(prefers-color-scheme: dark)');
if (mqList.addEventListener) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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