Compare commits

...
26 Commits
Author SHA1 Message Date
ssongliu c2879f2a56 fix: 增加前端注入校验规则 (#1456) 2023-06-26 17:25:18 +08:00
zhengkunwang223 67221e7f70 feat: 应用商店增加分页 (#1447) 2023-06-25 22:24:39 +08:00
zhengkunwang223 c76cc07c1c fix: 解决 Docker 版本过低导致应用无法操作的问题 (#1446)
Refs https://github.com/1Panel-dev/1Panel/issues/1445
2023-06-25 22:24:33 +08:00
ssongliu 7a091a152e fix: 解决计划任务备份根目录下文件夹失败的问题 (#1444) 2023-06-25 22:24:29 +08:00
ssongliu d2087641f7 fix: 解决终端连接注入漏洞问题 2023-06-25 22:24:22 +08:00
ssongliu 321ed00734 fix: 解决添加仓库注入漏洞问题 2023-06-25 22:24:19 +08:00
ssongliu 8ff160408f fix: 修改数据库无法正常访问情况下系统提示信息 (#1438) 2023-06-25 22:24:04 +08:00
ssongliu 662eae1ba4 fix: 解决容器镜像加速或仓库空行导致 Docker 无法正常启用的问题 (#1437) 2023-06-25 22:23:58 +08:00
ssongliu f808e81958 fix: ssh 登录日志排序优化 (#1435) 2023-06-25 22:23:53 +08:00
wanghe-fit2cloud f161bcbc31 fix: goreleaser 显示声明 CGO_ENABLED=0 2023-06-16 18:36:36 +08:00
wanghe-fit2cloud 73f789062b feat: 静态编译不依赖 glibc 2023-06-16 12:08:15 +08:00
ssongliu 1fdf4ffd2f fix: 构建移除对 cgo 的依赖 (#1386) 2023-06-16 12:08:06 +08:00
吴小白 6d58fe7a1e feat: 不使用 cgo (#1382) 2023-06-16 12:07:56 +08:00
ssongliu c8bbfada8c fix: 解决容器、终端界面 ios 适配的问题 (#1385) 2023-06-15 19:32:03 +08:00
ssongliu 47dda4ac9f fix: 修改应用导入恢复时数据库连接信息 (#1367) 2023-06-13 08:32:12 +00:00
ssongliu b1d40960c4 fix: 容器存储卷目录按钮样式修改 (#1366) 2023-06-13 06:28:12 +00:00
ssongliu 5222caecf9 fix: 应用恢复去掉版本限制 (#1365) 2023-06-13 04:28:11 +00:00
ssongliu bc8d4cbb0f feat: 应用导入恢复增加提示信息 (#1362) 2023-06-13 02:46:11 +00:00
凹凸曼and凹凸曼 0bb31f6caf fix: 修复创建数据库时误删数据库用户的bug (#1358)
Co-authored-by: 凹凸曼 <xx@xx>
2023-06-13 10:29:47 +08:00
凹凸曼and凹凸曼 03c8e4d3cb feat: 容器存储卷快捷进入挂载点文件目录 #1325 (#1357)
Co-authored-by: 凹凸曼 <xx@xx>
2023-06-13 10:27:55 +08:00
凹凸曼and凹凸曼 68ad653545 fix: 修复无法读取ssh成功日志 (#1356)
Co-authored-by: 凹凸曼 <xx@xx>
2023-06-13 10:26:39 +08:00
凹凸曼and凹凸曼 fd5b34d644 fix: 修复英文语言包缺少"operateConfirm"的问题 (#1354)
Co-authored-by: 凹凸曼 <xx@xx>
2023-06-13 10:25:01 +08:00
吴小白 000096bc26 fix: 修正 actions 脚本 2023-06-12 21:16:54 +08:00
ssongliu 02023102d3 fix: 解决计划任务执行周期错误的问题 (#1349) 2023-06-12 10:42:11 +00:00
ssongliu 95bc3d9855 feat: 应用导入备份恢复优化 (#1347) 2023-06-12 09:48:11 +00:00
凹凸曼 f9fb16198b feat: 计划任务和容器日志的前端改进 (#1319)
* feat: 计划任务增加批量开启和停止 #1010 

* feat: 容器日志增加全屏按钮 #1176
2023-06-12 15:44:15 +08:00
40 changed files with 485 additions and 209 deletions
+2 -4
View File
@@ -12,9 +12,7 @@ jobs:
build-linux-binary:
runs-on: ubuntu-latest
steps:
- name: Install cross-compilers
run: sudo apt-get update && sudo apt-get -y install gcc-x86-64-linux-gnu gcc-aarch64-linux-gnu gcc-arm-linux-gnueabi gcc-powerpc64le-linux-gnu gcc-s390x-linux-gnu
- name: Checkout code
- name: Checkout Code
uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
@@ -23,7 +21,7 @@ jobs:
- name: Build Web
id: build_frontend
run: |
cd frontend && npm install && npm run build:dev
cd frontend && npm install && npm run build:pro
env:
NODE_OPTIONS: --max-old-space-size=8192
- name: Setup Go
+21 -7
View File
@@ -10,9 +10,7 @@ jobs:
create-release:
runs-on: ubuntu-latest
steps:
- name: Install cross-compilers
run: sudo apt-get update && sudo apt-get -y install gcc-x86-64-linux-gnu gcc-aarch64-linux-gnu gcc-arm-linux-gnueabi gcc-powerpc64le-linux-gnu gcc-s390x-linux-gnu
- name: Checkout code
- name: Checkout Code
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v3
@@ -20,7 +18,7 @@ jobs:
node-version: '18.14'
- name: Build Web
run: |
cd frontend && npm install && npm run build:dev
cd frontend && npm install && npm run build:pro
env:
NODE_OPTIONS: --max-old-space-size=8192
- name: Setup Go
@@ -30,17 +28,33 @@ jobs:
- name: Build Release
uses: goreleaser/goreleaser-action@v4
with:
distribution: goreleaser
version: latest
args: release --skip-publish --clean
- name: Upload Assets
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
draft: true
body: |
# 一、安装和升级
## 1.1 一键安装
```sh
curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh && sudo bash quick_start.sh
```
## 1.2 在线升级
登录 1Panel Web 控制台,在页面右下角点击 **【检查更新】** 进行在线升级。
>更多信息请查阅在线文档:https://1panel.cn/docs/
# 二、更新日志
files: |
dist/*.tar.gz
dist/checksums.txt
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Setup OSSUTIL
uses: yizhoumo/setup-ossutil@v1
with:
@@ -49,4 +63,4 @@ jobs:
access-key-secret: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
ossutil-version: '1.7.14'
- name: Upload Assets to OSS
- run: ossutil cp -r dist/ oss://resource-fit2cloud-com/1panel/package/stable/${{ github.ref_name }}/release/ --include "*.tar.gz" --include "checksums.txt" --only-current-dir
run: ossutil cp -r dist/ oss://resource-fit2cloud-com/1panel/package/stable/${{ github.ref_name }}/release/ --include "*.tar.gz" --include "checksums.txt" --only-current-dir --force
+1
View File
@@ -30,5 +30,6 @@ dist/
1pctl
1panel.service
install.sh
quick_start.sh
cmd/server/web/.DS_Store
cmd/server/.DS_Store
+1 -11
View File
@@ -16,18 +16,8 @@ builds:
- -trimpath
ldflags:
- -w -s
- --extldflags "-static -fpic"
tags:
- osusergo
env:
- CGO_ENABLED=1
- >-
{{- if eq .Arch "amd64"}}CC=x86_64-linux-gnu-gcc{{- end }}
{{- if eq .Arch "arm64"}}CC=aarch64-linux-gnu-gcc{{- end }}
{{- if eq .Arch "arm"}}CC=arm-linux-gnueabi-gcc{{- end }}
{{- if eq .Arch "loong64"}}CC=loongarch64-linux-gnu-gcc{{- end }}
{{- if eq .Arch "ppc64le"}}CC=powerpc64le-linux-gnu-gcc{{- end }}
{{- if eq .Arch "s390x"}}CC=s390x-linux-gnu-gcc{{- end }}
- CGO_ENABLED=0
goos:
- linux
goarm:
-34
View File
@@ -1,34 +0,0 @@
FROM node:18.14 as build_web
ARG TARGETARCH
ARG NPM_REGISTRY="https://registry.npmmirror.com"
ENV NODE_OPTIONS="--max-old-space-size=4096"
WORKDIR /data
RUN set -ex \
&& npm config set registry ${NPM_REGISTRY}
ADD . /data
RUN set -ex \
&& cd /data/frontend \
&& npm install
RUN set -ex \
&& cd /data/frontend \
&& npm run build:dev
FROM golang:1.20
ARG TARGETARCH
ARG GOPROXY="https://goproxy.cn,direct"
COPY --from=build_web /data /data
WORKDIR /data
RUN set -ex \
&& go env -w GOPROXY=${GOPROXY} \
&& go install github.com/goreleaser/goreleaser@latest \
&& goreleaser build --single-target --snapshot --clean
CMD ["/bin/bash"]
+5 -5
View File
@@ -23,16 +23,16 @@ build_frontend:
build_backend_on_linux:
cd $(SERVER_PATH) \
&& CGO_ENABLED=1 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -tags 'osusergo,netgo' -o $(BUILD_PATH)/$(APP_NAME) $(MAIN)
&& GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(APP_NAME) $(MAIN)
build_backend_on_darwin:
cd $(SERVER_PATH) \
&& CGO_ENABLED=1 GOOS=linux GOARCH=amd64 CC=x86_64-linux-musl-gcc CXX=x86_64-linux-musl-g++ $(GOBUILD) -trimpath -ldflags '-s -w --extldflags "-static -fpic"' -o $(BUILD_PATH)/$(APP_NAME) $(MAIN)
&& GOOS=linux GOARCH=amd64 $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(APP_NAME) $(MAIN)
build_backend_on_archlinux:
cd $(SERVER_PATH) \
&& CGO_ENABLED=1 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w --extldflags "-fpic"' -tags osusergo -o $(BUILD_PATH)/$(APP_NAME) $(MAIN)
&& GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(APP_NAME) $(MAIN)
build_all: build_frontend build_backend_on_linux
build_all: build_frontend build_backend_on_linux
build_on_local: clean_assets build_frontend build_backend_on_darwin upx_bin
build_on_local: clean_assets build_frontend build_backend_on_darwin upx_bin
+8 -3
View File
@@ -163,11 +163,16 @@ func (b *BaseApi) ContainerWsSsh(c *gin.Context) {
}
defer wsConn.Close()
cmds := fmt.Sprintf("docker exec %s %s", containerID, command)
cmds := []string{"exec", containerID, command}
if len(user) != 0 {
cmds = fmt.Sprintf("docker exec -u %s %s %s", user, containerID, command)
cmds = []string{"exec", "-u", user, containerID, command}
}
stdout, err := cmd.Exec(cmds)
if cmd.CheckIllegal(user, containerID, command) {
if wshandleError(wsConn, errors.New(" The command contains illegal characters.")) {
return
}
}
stdout, err := cmd.ExecWithCheck("docker", cmds...)
if wshandleError(wsConn, errors.WithMessage(err, stdout)) {
return
}
+2 -2
View File
@@ -6,7 +6,7 @@ type CronjobCreate struct {
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`
SpecType string `json:"specType" validate:"required"`
Week int `json:"week" validate:"number,max=7,min=1"`
Week int `json:"week" validate:"number,max=6,min=0"`
Day int `json:"day" validate:"number"`
Hour int `json:"hour" validate:"number"`
Minute int `json:"minute" validate:"number"`
@@ -27,7 +27,7 @@ type CronjobUpdate struct {
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
SpecType string `json:"specType" validate:"required"`
Week int `json:"week" validate:"number,max=7,min=1"`
Week int `json:"week" validate:"number,max=6,min=0"`
Day int `json:"day" validate:"number"`
Hour int `json:"hour" validate:"number"`
Minute int `json:"minute" validate:"number"`
+9
View File
@@ -19,6 +19,7 @@ type IAppInstallResourceRpo interface {
GetFirst(opts ...DBOption) (model.AppInstallResource, error)
Create(ctx context.Context, resource *model.AppInstallResource) error
DeleteBy(ctx context.Context, opts ...DBOption) error
BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error
}
func NewIAppInstallResourceRpo() IAppInstallResourceRpo {
@@ -71,3 +72,11 @@ func (a AppInstallResourceRpo) Create(ctx context.Context, resource *model.AppIn
func (a AppInstallResourceRpo) DeleteBy(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.AppInstallResource{}).Error
}
func (a *AppInstallResourceRpo) BatchUpdateBy(maps map[string]interface{}, opts ...DBOption) error {
db := getDb(opts...).Model(&model.AppInstallResource{})
if len(opts) == 0 {
db = db.Where("1=1")
}
return db.Updates(&maps).Error
}
+59 -4
View File
@@ -148,7 +148,7 @@ func handleAppRecover(install *model.AppInstall, recoverFile string, isRollback
if err := json.Unmarshal(appjson, &oldInstall); err != nil {
return fmt.Errorf("unmarshal app.json failed, err: %v", err)
}
if oldInstall.App.Key != install.App.Key || oldInstall.Name != install.Name || oldInstall.Version != install.Version || oldInstall.ID != install.ID {
if oldInstall.App.Key != install.App.Key || oldInstall.Name != install.Name {
return errors.New("the current backup file does not match the application")
}
@@ -172,6 +172,7 @@ func handleAppRecover(install *model.AppInstall, recoverFile string, isRollback
}()
}
newEnvFile := ""
resource, _ := appInstallResourceRepo.GetFirst(appInstallResourceRepo.WithAppInstallId(install.ID))
if resource.ID != 0 && install.App.Key != "mysql" {
mysqlInfo, err := appInstallRepo.LoadBaseInfo(resource.Key, "")
@@ -182,7 +183,22 @@ func handleAppRecover(install *model.AppInstall, recoverFile string, isRollback
if err != nil {
return err
}
if err := handleMysqlRecover(mysqlInfo, tmpPath, db.Name, fmt.Sprintf("%s.sql.gz", install.Name), true); err != nil {
newDB, envMap, err := reCreateDB(db.ID, oldInstall.Env)
if err != nil {
return err
}
oldHost := fmt.Sprintf("\"PANEL_DB_HOST\":\"%v\"", envMap["PANEL_DB_HOST"].(string))
newHost := fmt.Sprintf("\"PANEL_DB_HOST\":\"%v\"", mysqlInfo.ServiceName)
oldInstall.Env = strings.ReplaceAll(oldInstall.Env, oldHost, newHost)
envMap["PANEL_DB_HOST"] = mysqlInfo.ServiceName
newEnvFile, err = coverEnvJsonToStr(oldInstall.Env)
if err != nil {
return err
}
_ = appInstallResourceRepo.BatchUpdateBy(map[string]interface{}{"resource_id": newDB.ID}, commonRepo.WithByID(resource.ID))
if err := handleMysqlRecover(mysqlInfo, tmpPath, newDB.Name, fmt.Sprintf("%s.sql.gz", install.Name), true); err != nil {
global.LOG.Errorf("handle recover from sql.gz failed, err: %v", err)
return err
}
@@ -193,11 +209,50 @@ func handleAppRecover(install *model.AppInstall, recoverFile string, isRollback
return err
}
oldInstall.Status = constant.Running
if err := appInstallRepo.Save(context.Background(), install); err != nil {
if len(newEnvFile) != 0 {
envPath := fmt.Sprintf("%s/%s/%s/.env", constant.AppInstallDir, install.App.Key, install.Name)
file, err := os.OpenFile(envPath, os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil {
return err
}
defer file.Close()
_, _ = file.WriteString(newEnvFile)
}
oldInstall.ID = install.ID
oldInstall.Status = constant.StatusRunning
oldInstall.AppId = install.AppId
oldInstall.AppDetailId = install.AppDetailId
if err := appInstallRepo.Save(context.Background(), &oldInstall); err != nil {
global.LOG.Errorf("save db app install failed, err: %v", err)
return err
}
isOk = true
return nil
}
func reCreateDB(dbID uint, oldEnv string) (*model.DatabaseMysql, map[string]interface{}, error) {
mysqlService := NewIMysqlService()
ctx := context.Background()
_ = mysqlService.Delete(ctx, dto.MysqlDBDelete{ID: dbID, DeleteBackup: true, ForceDelete: true})
envMap := make(map[string]interface{})
if err := json.Unmarshal([]byte(oldEnv), &envMap); err != nil {
return nil, envMap, err
}
oldName, _ := envMap["PANEL_DB_NAME"].(string)
oldUser, _ := envMap["PANEL_DB_USER"].(string)
oldPassword, _ := envMap["PANEL_DB_USER_PASSWORD"].(string)
createDB, err := mysqlService.Create(context.Background(), dto.MysqlDBCreate{
Name: oldName,
Format: "utf8mb4",
Username: oldUser,
Password: oldPassword,
Permission: "%",
})
if err != nil {
return nil, envMap, err
}
return createDB, envMap, nil
}
+3
View File
@@ -197,6 +197,9 @@ func handleTar(sourceDir, targetDir, name, exclusionRules string) error {
if strings.Contains(sourceDir, "/") {
itemDir := strings.ReplaceAll(sourceDir[strings.LastIndex(sourceDir, "/"):], "/", "")
aheadDir := sourceDir[:strings.LastIndex(sourceDir, "/")]
if len(aheadDir) == 0 {
aheadDir = "/"
}
path += fmt.Sprintf("-C %s %s", aheadDir, itemDir)
} else {
path = sourceDir
+16 -14
View File
@@ -14,7 +14,6 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/app/repo"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
@@ -100,7 +99,7 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
}
return nil, err
}
if err := u.createUser(app, req); err != nil {
if err := u.createUser(app.ContainerName, app.Password, app.Version, req); err != nil {
return nil, err
}
@@ -291,7 +290,7 @@ func (u *MysqlService) ChangeAccess(info dto.ChangeDBInfo) error {
}
}
if err := u.createUser(app, dto.MysqlDBCreate{
if err := u.createUser(app.ContainerName, app.Password, app.Version, dto.MysqlDBCreate{
Username: mysql.Username,
Name: mysql.Name,
Permission: info.Value,
@@ -470,7 +469,7 @@ func (u *MysqlService) LoadStatus() (*dto.MysqlStatus, error) {
return &info, nil
}
func (u *MysqlService) createUser(app *repo.RootInfo, req dto.MysqlDBCreate) error {
func (u *MysqlService) createUser(container, password, version string, req dto.MysqlDBCreate) error {
var userlist []string
if strings.Contains(req.Permission, ",") {
ips := strings.Split(req.Permission, ",")
@@ -484,32 +483,35 @@ func (u *MysqlService) createUser(app *repo.RootInfo, req dto.MysqlDBCreate) err
}
for _, user := range userlist {
if err := excSQL(app.ContainerName, app.Password, fmt.Sprintf("create user %s identified by '%s';", user, req.Password)); err != nil {
handleCreateError(req.Name, userlist, app)
if err := excSQL(container, password, fmt.Sprintf("create user %s identified by '%s';", user, req.Password)); err != nil {
if strings.Contains(err.Error(), "ERROR 1396") {
handleCreateError(container, password, req.Name, userlist, false)
return buserr.New(constant.ErrUserIsExist)
}
handleCreateError(container, password, req.Name, userlist, true)
return err
}
grantStr := fmt.Sprintf("grant all privileges on `%s`.* to %s", req.Name, user)
if req.Name == "*" {
grantStr = fmt.Sprintf("grant all privileges on *.* to %s", user)
}
if strings.HasPrefix(app.Version, "5.7") {
if strings.HasPrefix(version, "5.7") {
grantStr = fmt.Sprintf("%s identified by '%s' with grant option;", grantStr, req.Password)
}
if err := excSQL(app.ContainerName, app.Password, grantStr); err != nil {
handleCreateError(req.Name, userlist, app)
if err := excSQL(container, password, grantStr); err != nil {
handleCreateError(container, password, req.Name, userlist, true)
return err
}
}
return nil
}
func handleCreateError(dbName string, userlist []string, app *repo.RootInfo) {
_ = excSQL(app.ContainerName, app.Password, fmt.Sprintf("drop database `%s`", dbName))
for _, user := range userlist {
if err := excSQL(app.ContainerName, app.Password, fmt.Sprintf("drop user if exists %s", user)); err != nil {
global.LOG.Errorf("drop user failed, err: %v", err)
func handleCreateError(contaienr, password, dbName string, userlist []string, dropUser bool) {
_ = excSQL(contaienr, password, fmt.Sprintf("drop database `%s`", dbName))
if dropUser {
for _, user := range userlist {
if err := excSQL(contaienr, password, fmt.Sprintf("drop user if exists %s", user)); err != nil {
global.LOG.Errorf("drop user failed, err: %v", err)
}
}
}
}
+2
View File
@@ -136,12 +136,14 @@ func (u *DockerService) UpdateConf(req dto.SettingUpdate) error {
switch req.Key {
case "Registries":
req.Value = strings.TrimRight(req.Value, ",")
if len(req.Value) == 0 {
delete(daemonMap, "insecure-registries")
} else {
daemonMap["insecure-registries"] = strings.Split(req.Value, ",")
}
case "Mirrors":
req.Value = strings.TrimRight(req.Value, ",")
if len(req.Value) == 0 {
delete(daemonMap, "registry-mirrors")
} else {
+11 -3
View File
@@ -3,12 +3,14 @@ package service
import (
"context"
"encoding/json"
"fmt"
"os"
"path"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
@@ -76,6 +78,9 @@ func (u *ImageRepoService) List() ([]dto.ImageRepoOption, error) {
}
func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
if cmd.CheckIllegal(req.Username, req.Password, req.DownloadUrl) {
return buserr.New(constant.ErrRepoConn)
}
imageRepo, _ := imageRepoRepo.Get(commonRepo.WithByName(req.Name))
if imageRepo.ID != 0 {
return constant.ErrRecordExist
@@ -142,6 +147,9 @@ func (u *ImageRepoService) Update(req dto.ImageRepoUpdate) error {
if req.ID == 1 {
return errors.New("The default value cannot be deleted !")
}
if cmd.CheckIllegal(req.Username, req.Password, req.DownloadUrl) {
return buserr.New(constant.ErrRepoConn)
}
repo, err := imageRepoRepo.Get(commonRepo.WithByID(req.ID))
if err != nil {
return err
@@ -149,7 +157,7 @@ func (u *ImageRepoService) Update(req dto.ImageRepoUpdate) error {
if repo.DownloadUrl != req.DownloadUrl || (!repo.Auth && req.Auth) {
_ = u.handleRegistries(req.DownloadUrl, repo.DownloadUrl, "update")
if repo.Auth {
_, _ = cmd.Execf("docker logout %s", repo.DownloadUrl)
_, _ = cmd.ExecWithCheck("docker", "logout", repo.DownloadUrl)
}
stdout, err := cmd.Exec("systemctl restart docker")
if err != nil {
@@ -176,9 +184,9 @@ func (u *ImageRepoService) Update(req dto.ImageRepoUpdate) error {
}
func (u *ImageRepoService) CheckConn(host, user, password string) error {
stdout, err := cmd.Execf("docker login -u %s -p %s %s", user, password, host)
stdout, err := cmd.ExecWithCheck("docker", "login", "-u", user, "-p", password, host)
if err != nil {
return errors.New(string(stdout))
return fmt.Errorf("stdout: %s, stderr: %v", stdout, err)
}
if strings.Contains(string(stdout), "Login Succeeded") {
return nil
+97 -71
View File
@@ -229,29 +229,29 @@ func (u *SSHService) LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error) {
if len(req.Info) != 0 {
command = fmt.Sprintf(" | grep '%s'", req.Info)
}
for i := 0; i < len(fileList); i++ {
if strings.HasPrefix(path.Base(fileList[i]), "secure") {
commandItem := fmt.Sprintf("cat %s | grep -a 'Failed password for' | grep -v 'invalid' %s", fileList[i], command)
dataItem := loadFailedSecureDatas(commandItem)
data.FailedCount += len(dataItem)
data.TotalCount += len(dataItem)
if req.Status != constant.StatusSuccess {
withAppend := len(data.Logs) < req.Page*req.PageSize
if req.Status != constant.StatusSuccess {
if strings.HasPrefix(path.Base(fileList[i]), "secure") {
commandItem := fmt.Sprintf("cat %s | grep -a 'Failed password for' | grep -v 'invalid' %s", fileList[i], command)
dataItem, itemTotal := loadFailedSecureDatas(commandItem, withAppend)
data.FailedCount += itemTotal
data.TotalCount += itemTotal
data.Logs = append(data.Logs, dataItem...)
}
if strings.HasPrefix(path.Base(fileList[i]), "auth.log") {
commandItem := fmt.Sprintf("cat %s | grep -a 'Connection closed by authenticating user' | grep -a 'preauth' %s", fileList[i], command)
dataItem, itemTotal := loadFailedAuthDatas(commandItem, withAppend)
data.FailedCount += itemTotal
data.TotalCount += itemTotal
data.Logs = append(data.Logs, dataItem...)
}
}
if strings.HasPrefix(path.Base(fileList[i]), "auth.log") {
commandItem := fmt.Sprintf("cat %s | grep -a 'Connection closed by authenticating user' | grep -a 'preauth' %s", fileList[i], command)
dataItem := loadFailedAuthDatas(commandItem)
data.FailedCount += len(dataItem)
data.TotalCount += len(dataItem)
if req.Status != constant.StatusSuccess {
data.Logs = append(data.Logs, dataItem...)
}
}
commandItem := fmt.Sprintf("cat %s | grep Accepted %s", fileList[i], command)
dataItem := loadSuccessDatas(commandItem)
data.TotalCount += len(dataItem)
if req.Status != constant.StatusFailed {
commandItem := fmt.Sprintf("cat %s | grep -a Accepted %s", fileList[i], command)
dataItem, itemTotal := loadSuccessDatas(commandItem, withAppend)
data.TotalCount += itemTotal
data.Logs = append(data.Logs, dataItem...)
}
}
@@ -279,7 +279,7 @@ func (u *SSHService) LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error) {
global.LOG.Errorf("load qqwry datas failed: %s", err)
}
var itemLogs []dto.SSHHistory
for i := len(data.Logs) - 1; i >= 0; i-- {
for i := 0; i < len(data.Logs); i++ {
data.Logs[i].Area = qqWry.Find(data.Logs[i].Address).Area
data.Logs[i].Date, _ = time.ParseInLocation("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s", timeNow.Year(), data.Logs[i].DateStr), nyc)
itemLogs = append(itemLogs, data.Logs[i])
@@ -293,18 +293,17 @@ func sortFileList(fileNames []string) []string {
if len(fileNames) < 2 {
return fileNames
}
var itemFile []string
if strings.Contains(fileNames[0], "secure") {
if strings.HasPrefix(path.Base(fileNames[0]), "secure") {
var itemFile []string
sort.Slice(fileNames, func(i, j int) bool {
return fileNames[i] < fileNames[j]
return fileNames[i] > fileNames[j]
})
itemFile = append(itemFile, fileNames[1:]...)
itemFile = append(itemFile, fileNames[0])
itemFile = append(itemFile, fileNames[len(fileNames)-1])
itemFile = append(itemFile, fileNames[:len(fileNames)-2]...)
return itemFile
}
sort.Slice(fileNames, func(i, j int) bool {
return fileNames[i] > fileNames[j]
return fileNames[i] < fileNames[j]
})
return fileNames
}
@@ -339,82 +338,109 @@ func updateSSHConf(oldFiles []string, param string, value interface{}) []string
return newFiles
}
func loadSuccessDatas(command string) []dto.SSHHistory {
var datas []dto.SSHHistory
func loadSuccessDatas(command string, withAppend bool) ([]dto.SSHHistory, int) {
var (
datas []dto.SSHHistory
totalNum int
)
stdout2, err := cmd.Exec(command)
if err == nil {
lines := strings.Split(string(stdout2), "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(lines) == 0 {
return datas, 0
}
for i := len(lines) - 1; i >= 0; i-- {
parts := strings.Fields(lines[i])
if len(parts) < 14 {
continue
}
historyItem := dto.SSHHistory{
DateStr: fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]),
AuthMode: parts[6],
User: parts[8],
Address: parts[10],
Port: parts[12],
Status: constant.StatusSuccess,
totalNum++
if withAppend {
historyItem := dto.SSHHistory{
DateStr: fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]),
AuthMode: parts[6],
User: parts[8],
Address: parts[10],
Port: parts[12],
Status: constant.StatusSuccess,
}
datas = append(datas, historyItem)
}
datas = append(datas, historyItem)
}
}
return datas
return datas, totalNum
}
func loadFailedAuthDatas(command string) []dto.SSHHistory {
var datas []dto.SSHHistory
func loadFailedAuthDatas(command string, withAppend bool) ([]dto.SSHHistory, int) {
var (
datas []dto.SSHHistory
totalNum int
)
stdout2, err := cmd.Exec(command)
if err == nil {
lines := strings.Split(string(stdout2), "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(lines) == 0 {
return datas, 0
}
for i := len(lines) - 1; i >= 0; i-- {
parts := strings.Fields(lines[i])
if len(parts) < 14 {
continue
}
historyItem := dto.SSHHistory{
DateStr: fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]),
AuthMode: parts[8],
User: parts[10],
Address: parts[11],
Port: parts[13],
Status: constant.StatusFailed,
totalNum++
if withAppend {
historyItem := dto.SSHHistory{
DateStr: fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]),
AuthMode: parts[8],
User: parts[10],
Address: parts[11],
Port: parts[13],
Status: constant.StatusFailed,
}
if strings.Contains(lines[i], ": ") {
historyItem.Message = strings.Split(lines[i], ": ")[1]
}
datas = append(datas, historyItem)
}
if strings.Contains(line, ": ") {
historyItem.Message = strings.Split(line, ": ")[1]
}
datas = append(datas, historyItem)
}
}
return datas
return datas, totalNum
}
func loadFailedSecureDatas(command string) []dto.SSHHistory {
var datas []dto.SSHHistory
func loadFailedSecureDatas(command string, withAppend bool) ([]dto.SSHHistory, int) {
var (
datas []dto.SSHHistory
totalNum int
)
stdout2, err := cmd.Exec(command)
if err == nil {
lines := strings.Split(string(stdout2), "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(lines) == 0 {
return datas, 0
}
for i := len(lines) - 1; i >= 0; i-- {
parts := strings.Fields(lines[i])
if len(parts) < 14 {
continue
}
historyItem := dto.SSHHistory{
DateStr: fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]),
AuthMode: parts[6],
User: parts[8],
Address: parts[10],
Port: parts[12],
Status: constant.StatusFailed,
totalNum++
if withAppend {
historyItem := dto.SSHHistory{
DateStr: fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]),
AuthMode: parts[6],
User: parts[8],
Address: parts[10],
Port: parts[12],
Status: constant.StatusFailed,
}
if strings.Contains(lines[i], ": ") {
historyItem.Message = strings.Split(lines[i], ": ")[1]
}
datas = append(datas, historyItem)
}
if strings.Contains(line, ": ") {
historyItem.Message = strings.Split(line, ": ")[1]
}
datas = append(datas, historyItem)
}
}
return datas
return datas, totalNum
}
func handleGunzip(path string) error {
+1
View File
@@ -105,6 +105,7 @@ var (
ErrInUsed = "ErrInUsed"
ErrObjectInUsed = "ErrObjectInUsed"
ErrPortRules = "ErrPortRules"
ErrRepoConn = "ErrRepoConn"
)
// runtime
+1
View File
@@ -78,6 +78,7 @@ ErrTypeOfRedis: "The recovery file type does not match the current persistence m
#container
ErrInUsed: "{{ .detail }} is in use and cannot be deleted"
ErrObjectInUsed: "This object is in use and cannot be deleted"
ErrRepoConn: "The repository information contains illegal characters"
#runtime
ErrDirNotFound: "The build folder does not exist! Please check file integrity!"
+1
View File
@@ -78,6 +78,7 @@ ErrTypeOfRedis: "恢复文件类型与当前持久化方式不符,请修改后
#container
ErrInUsed: "{{ .detail }} 正被使用,无法删除"
ErrObjectInUsed: "该对象正被使用,无法删除"
ErrRepoConn: "仓库信息中存在不合法的字符"
#runtime
ErrDirNotFound: "build 文件夹不存在!请检查文件完整性!"
+3 -2
View File
@@ -2,12 +2,13 @@ package db
import (
"fmt"
"gorm.io/gorm/logger"
"log"
"os"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm/logger"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"github.com/1Panel-dev/1Panel/backend/global"
+1 -1
View File
@@ -15,7 +15,7 @@ func BindDomain() gin.HandlerFunc {
settingRepo := repo.NewISettingRepo()
status, err := settingRepo.Get(settingRepo.WithByKey("BindDomain"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrDomain, constant.ErrTypeInternalServer, err)
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
if len(status.Value) == 0 {
+1 -1
View File
@@ -15,7 +15,7 @@ func WhiteAllow() gin.HandlerFunc {
settingRepo := repo.NewISettingRepo()
status, err := settingRepo.Get(settingRepo.WithByKey("AllowIPs"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrIP, constant.ErrTypeInternalServer, err)
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
+2 -2
View File
@@ -16,7 +16,7 @@ func PasswordExpired() gin.HandlerFunc {
settingRepo := repo.NewISettingRepo()
setting, err := settingRepo.Get(settingRepo.WithByKey("ExpirationDays"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodePasswordExpired, constant.ErrTypePasswordExpired, err)
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypePasswordExpired, err)
return
}
expiredDays, _ := strconv.Atoi(setting.Value)
@@ -27,7 +27,7 @@ func PasswordExpired() gin.HandlerFunc {
extime, err := settingRepo.Get(settingRepo.WithByKey("ExpirationTime"))
if err != nil {
helper.ErrorWithDetail(c, constant.CodePasswordExpired, constant.ErrTypePasswordExpired, err)
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypePasswordExpired, err)
return
}
loc, _ := time.LoadLocation(common.LoadTimeZone())
+38
View File
@@ -5,6 +5,7 @@ import (
"context"
"fmt"
"os/exec"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/buserr"
@@ -117,6 +118,43 @@ func Execf(cmdStr string, a ...interface{}) (string, error) {
return stdout.String(), nil
}
func ExecWithCheck(name string, a ...string) (string, error) {
cmd := exec.Command(name, a...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
errMsg := ""
if len(stderr.String()) != 0 {
errMsg = fmt.Sprintf("stderr: %s", stderr.String())
}
if len(stdout.String()) != 0 {
if len(errMsg) != 0 {
errMsg = fmt.Sprintf("%s; stdout: %s", errMsg, stdout.String())
} else {
errMsg = fmt.Sprintf("stdout: %s", stdout.String())
}
}
return errMsg, err
}
return stdout.String(), nil
}
func CheckIllegal(args ...string) bool {
if args == nil {
return false
}
for _, arg := range args {
if strings.Contains(arg, "&") || strings.Contains(arg, "|") || strings.Contains(arg, ";") ||
strings.Contains(arg, "$") || strings.Contains(arg, "'") || strings.Contains(arg, "`") ||
strings.Contains(arg, "(") || strings.Contains(arg, ")") || strings.Contains(arg, "\"") {
return true
}
}
return false
}
func HasNoPasswordSudo() bool {
cmd2 := exec.Command("sudo", "-n", "ls")
err2 := cmd2.Run()
+6 -6
View File
@@ -5,31 +5,31 @@ import (
)
func Up(filePath string) (string, error) {
stdout, err := cmd.Execf("docker compose -f %s up -d", filePath)
stdout, err := cmd.Execf("docker-compose -f %s up -d", filePath)
return stdout, err
}
func Down(filePath string) (string, error) {
stdout, err := cmd.Execf("docker compose -f %s down --remove-orphans", filePath)
stdout, err := cmd.Execf("docker-compose -f %s down --remove-orphans", filePath)
return stdout, err
}
func Start(filePath string) (string, error) {
stdout, err := cmd.Execf("docker compose -f %s start", filePath)
stdout, err := cmd.Execf("docker-compose -f %s start", filePath)
return stdout, err
}
func Stop(filePath string) (string, error) {
stdout, err := cmd.Execf("docker compose -f %s stop", filePath)
stdout, err := cmd.Execf("docker-compose -f %s stop", filePath)
return stdout, err
}
func Restart(filePath string) (string, error) {
stdout, err := cmd.Execf("docker compose -f %s restart", filePath)
stdout, err := cmd.Execf("docker-compose -f %s restart", filePath)
return stdout, err
}
func Operate(filePath, operation string) (string, error) {
stdout, err := cmd.Execf("docker compose -f %s %s", filePath, operation)
stdout, err := cmd.Execf("docker-compose -f %s %s", filePath, operation)
return stdout, err
}
+1 -1
View File
@@ -7,8 +7,8 @@ import (
"github.com/1Panel-dev/1Panel/backend/server"
cmdUtils "github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/glebarez/sqlite"
"github.com/spf13/cobra"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
@@ -9,6 +9,9 @@
<el-divider v-if="slots.buttons" direction="vertical" />
<slot v-if="slots.buttons" name="buttons"></slot>
</template>
<template #extra>
<slot v-if="slots.extra" name="extra"></slot>
</template>
</el-page-header>
</template>
+12 -3
View File
@@ -18,13 +18,13 @@
:stroke-width="12"
:percentage="uploadPrecent"
></el-progress>
<div v-if="type === 'mysql'" class="el-upload__tip">
<div v-if="type === 'mysql'" style="width: 80%" class="el-upload__tip">
<span class="input-help">{{ $t('database.supportUpType') }}</span>
<span class="input-help">
{{ $t('database.zipFormat') }}
</span>
</div>
<div v-else class="el-upload__tip">
<div v-else style="width: 80%" class="el-upload__tip">
<span class="input-help">{{ $t('website.supportUpType') }}</span>
<span class="input-help">
{{ $t('website.zipFormat', [type + '.json']) }}
@@ -280,7 +280,16 @@ const buttons = [
{
label: i18n.global.t('commons.button.recover'),
click: (row: File.File) => {
onRecover(row);
if (type.value !== 'app') {
onRecover(row);
} else {
ElMessageBox.confirm(i18n.global.t('app.restoreWarn'), i18n.global.t('commons.button.recover'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
onRecover(row);
});
}
},
},
{
+27
View File
@@ -30,6 +30,27 @@ const checkHost = (rule: any, value: any, callback: any) => {
}
};
const checkIllegal = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.requiredInput')));
return;
}
if (
value.indexOf('&') !== -1 ||
value.indexOf('|') !== -1 ||
value.indexOf(';') !== -1 ||
value.indexOf('$') !== -1 ||
value.indexOf("'") !== -1 ||
value.indexOf('`') !== -1 ||
value.indexOf('(') !== -1 ||
value.indexOf(')') !== -1
) {
callback(new Error(i18n.global.t('commons.rule.illegalInput')));
} else {
callback();
}
};
const complexityPassword = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.complexityPassword')));
@@ -333,6 +354,7 @@ interface CommonRule {
integerNumber: FormItemRule;
ip: FormItemRule;
host: FormItemRule;
illegal: FormItemRule;
port: FormItemRule;
domain: FormItemRule;
databaseName: FormItemRule;
@@ -440,6 +462,11 @@ export const Rules: CommonRule = {
required: true,
trigger: 'blur',
},
illegal: {
validator: checkIllegal,
required: true,
trigger: 'blur',
},
port: {
required: true,
trigger: 'blur',
+8 -3
View File
@@ -97,6 +97,7 @@ const message = {
updateSuccess: 'Update Success',
uploadSuccess: 'Update Success',
operate: 'Operate',
operateConfirm: 'If you are sure about the operation, please input it manually',
inputOrSelect: 'Please select or enter',
backupSuccess: 'Backup Success',
unSupportType: 'Current file type is not supported!',
@@ -134,6 +135,7 @@ const message = {
rePassword: 'The passwords are inconsistent. Please check and re-enter the password',
requiredInput: 'Please enter the required fields',
requiredSelect: 'Please select the required fields',
illegalInput: 'There are illegal characters in the input box.',
commonName: 'Support English, Chinese, numbers, .-, and _ length 1-30',
userName: 'Support English, Chinese, numbers and _ length 3-30',
simpleName: 'Support English, numbers and _ length 1-30',
@@ -553,6 +555,7 @@ const message = {
cache: 'Cache',
volume: 'Volume',
volumeDir: 'Volume dir',
volumeName: 'Name',
nfsEnable: 'Enable NFS storage',
nfsAddress: 'Address',
@@ -632,8 +635,10 @@ const message = {
cronjob: {
cronTask: 'Task',
changeStatus: 'Change status',
disableMsg: 'The cronjob cannot continue to run after it is stopped. Do you want to stop it?',
enableMsg: 'The cronjob has been stopped. Enable now?',
disableMsg:
'Stopping the scheduled task will result in the task no longer automatically executing. Do you want to continue?',
enableMsg:
'Enabling the scheduled task will allow the task to automatically execute on a regular basis. Do you want to continue?',
taskType: 'Task type',
record: 'Records',
shell: 'Shell script',
@@ -1139,7 +1144,7 @@ const message = {
backupdate: 'Backup time',
restore: 'Restore',
restoreWarn:
'The restore operation will restart the application and replace the data. This operation cannot be rolled back. Do you want to continue?',
'The recovery operation will delete the current data of this application and perform a restart. This action is irreversible. Do you wish to continue?',
update: 'update',
upgrade: 'upgrade',
versionSelect: 'Please select a version',
+5 -3
View File
@@ -138,6 +138,7 @@ const message = {
rePassword: '密码不一致,请检查后重新输入',
requiredInput: '请填写必填项',
requiredSelect: '请选择必选项',
illegalInput: '输入框中存在不合法字符',
commonName: '支持英文、中文、数字、.-和_,长度1-30',
userName: '支持英文、中文、数字和_,长度3-30',
simpleName: '支持英文、数字、_,长度1-30',
@@ -564,6 +565,7 @@ const message = {
gateway: '网关',
volume: '存储卷',
volumeDir: '存储卷目录',
nfsEnable: '启用 NFS 存储',
nfsAddress: '地址',
nfsAddressHelper: '支持输入 ip 或者域名',
@@ -633,8 +635,8 @@ const message = {
cronjob: {
cronTask: '计划任务',
changeStatus: '状态修改',
disableMsg: '计划任务停止后将无法继续运行,是否停止?',
enableMsg: '该计划任务已停止,是否启用?',
disableMsg: '停止计划任务会导致该任务不再自动执行。是否继续?',
enableMsg: '启用计划任务会让该任务定期自动执行。是否继续?',
taskType: '任务类型',
record: '报告',
shell: 'Shell 脚本',
@@ -1109,7 +1111,7 @@ const message = {
backupPath: '文件路径',
backupdate: '备份时间',
restore: '恢复',
restoreWarn: '恢复操作会重启应用,并替换数据,此操作不可回滚,是否继续?',
restoreWarn: '恢复操作将删除该应用当前数据并重启。此操作不可回滚,是否继续?',
update: '更新',
upgrade: '升级',
versionSelect: '请选择版本',
@@ -99,6 +99,15 @@
</div>
</el-col>
</el-row>
<div class="page-button">
<fu-table-pagination
v-model:current-page="paginationConfig.currentPage"
v-model:page-size="paginationConfig.pageSize"
v-bind="paginationConfig"
@change="search(req)"
:layout="'total, sizes, prev, pager, next, jumper'"
/>
</div>
</template>
</LayoutContent>
<Detail v-if="showDetail" :id="appId"></Detail>
@@ -116,6 +125,12 @@ import { useI18n } from 'vue-i18n';
const language = useI18n().locale.value;
const paginationConfig = reactive({
currentPage: 1,
pageSize: 50,
total: 0,
});
const req = reactive({
name: '',
tags: [],
@@ -138,9 +153,12 @@ const getColor = (index: number) => {
const search = async (req: App.AppReq) => {
loading.value = true;
req.pageSize = paginationConfig.pageSize;
req.page = paginationConfig.currentPage;
await SearchApp(req)
.then((res) => {
apps.value = res.data.items;
paginationConfig.total = res.data.total;
})
.finally(() => {
loading.value = false;
@@ -260,4 +278,10 @@ onMounted(() => {
border: none;
}
}
.page-button {
float: right;
margin-bottom: 10px;
margin-top: 10px;
}
</style>
@@ -1,8 +1,17 @@
<template>
<div>
<el-drawer v-model="logVisiable" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
<el-drawer
v-model="logVisiable"
:destroy-on-close="true"
:close-on-click-modal="false"
:size="globalStore.isFullScreen ? '100%' : '50%'"
>
<template #header>
<DrawerHeader :header="$t('commons.button.log')" :resource="logSearch.container" :back="handleClose" />
<DrawerHeader :header="$t('commons.button.log')" :resource="logSearch.container" :back="handleClose">
<template #extra v-if="!mobile">
<el-button @click="toggleFullscreen" class="fullScreen" icon="FullScreen" plain></el-button>
</template>
</DrawerHeader>
</template>
<div>
<el-select @change="searchLogs" style="width: 30%; float: left" v-model="logSearch.mode">
@@ -47,23 +56,28 @@
import { cleanContainerLog, logContainer } from '@/api/modules/container';
import i18n from '@/lang';
import { dateFormatForName } from '@/utils/util';
import { nextTick, onBeforeUnmount, reactive, ref, shallowRef } from 'vue';
import { computed, nextTick, onBeforeUnmount, reactive, ref, shallowRef, watch } from 'vue';
import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { oneDark } from '@codemirror/theme-one-dark';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { ElMessageBox } from 'element-plus';
import { MsgSuccess } from '@/utils/message';
import screenfull from 'screenfull';
import { GlobalStore } from '@/store';
const extensions = [javascript(), oneDark];
const logVisiable = ref(false);
const mobile = computed(() => {
return globalStore.isMobile();
});
const logInfo = ref();
const view = shallowRef();
const handleReady = (payload) => {
view.value = payload.view;
};
const globalStore = GlobalStore();
const logSearch = reactive({
isWatch: false,
@@ -93,12 +107,19 @@ const timeOptions = ref([
},
]);
function toggleFullscreen() {
if (screenfull.isEnabled) {
screenfull.toggle();
}
}
const handleClose = async () => {
logVisiable.value = false;
clearInterval(Number(timer));
timer = null;
};
watch(logVisiable, (val) => {
if (screenfull.isEnabled && !val && !mobile.value) screenfull.exit();
});
const searchLogs = async () => {
const res = await logContainer(logSearch);
logInfo.value = res.data || '';
@@ -150,6 +171,12 @@ const acceptParams = (props: DialogProps): void => {
searchLogs();
}
}, 1000 * 5);
if (!mobile.value) {
screenfull.on('change', () => {
globalStore.isFullScreen = screenfull.isFullscreen;
});
}
};
onBeforeUnmount(() => {
@@ -166,4 +193,7 @@ defineExpose({
.margin-button {
margin-left: 20px;
}
.fullScreen {
border: none;
}
</style>
@@ -111,10 +111,10 @@ const handleClose = () => {
};
const rules = reactive({
name: [Rules.requiredInput, Rules.name],
downloadUrl: [Rules.requiredInput],
downloadUrl: [Rules.illegal],
protocol: [Rules.requiredSelect],
username: [Rules.requiredInput],
password: [Rules.requiredInput],
username: [Rules.illegal],
password: [Rules.illegal],
auth: [Rules.requiredSelect],
});
+12 -1
View File
@@ -49,6 +49,15 @@
<Tooltip @click="onInspect(row.name)" :text="row.name" />
</template>
</el-table-column>
<el-table-column :label="$t('container.volumeDir')" min-width="50">
<template #default="{ row }">
<el-button type="primary" link @click="toFolder(row.mountpoint)">
<el-icon>
<FolderOpened />
</el-icon>
</el-button>
</template>
</el-table-column>
<el-table-column
:label="$t('container.mountpoint')"
show-overflow-tooltip
@@ -124,7 +133,9 @@ const loadStatus = async () => {
const goSetting = async () => {
router.push({ name: 'ContainerSetting' });
};
const toFolder = (folder: string) => {
router.push({ path: '/hosts/files', query: { path: folder } });
};
const dialogCreateRef = ref<DialogExpose>();
interface DialogExpose {
+20
View File
@@ -15,6 +15,12 @@
<el-button type="primary" @click="onOpenDialog('create')">
{{ $t('commons.button.create') }}{{ $t('cronjob.cronTask') }}
</el-button>
<el-button plain :disabled="selects.length === 0" @click="onBatchChangeStatus('enable')">
{{ $t('commons.button.enable') }}
</el-button>
<el-button plain :disabled="selects.length === 0" @click="onBatchChangeStatus('disable')">
{{ $t('commons.button.disable') }}
</el-button>
<el-button plain :disabled="selects.length === 0" @click="onDelete(null)">
{{ $t('commons.button.delete') }}
</el-button>
@@ -320,6 +326,20 @@ const onChangeStatus = async (id: number, status: string) => {
});
};
const onBatchChangeStatus = async (status: string) => {
ElMessageBox.confirm(i18n.global.t('cronjob.' + status + 'Msg'), i18n.global.t('cronjob.changeStatus'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
let itemStatus = status === 'enable' ? 'Enable' : 'Disable';
for (const item of selects.value) {
await updateStatus({ id: item.id, status: itemStatus });
}
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
});
};
const onHandle = async (row: Cronjob.CronjobInfo) => {
loading.value = true;
await handleOnce(row.id)
+4 -2
View File
@@ -330,7 +330,7 @@ const weekOptions = [
{ label: i18n.global.t('cronjob.thursday'), value: 4 },
{ label: i18n.global.t('cronjob.friday'), value: 5 },
{ label: i18n.global.t('cronjob.saturday'), value: 6 },
{ label: i18n.global.t('cronjob.sunday'), value: 7 },
{ label: i18n.global.t('cronjob.sunday'), value: 0 },
];
const rules = reactive({
name: [Rules.requiredInput],
@@ -456,7 +456,9 @@ function checkScript() {
case 'perMonth':
return row.day > 0 && row.day < 32 && row.hour >= 0 && row.hour < 24 && row.minute >= 0 && row.minute < 60;
case 'perWeek':
return row.week > 0 && row.week < 8 && row.hour >= 0 && row.hour < 24 && row.minute >= 0 && row.minute < 60;
return (
row.week >= 0 && row.week < 7 && row.hour >= 0 && row.hour < 24 && row.minute >= 0 && row.minute < 60
);
case 'perDay':
return row.hour >= 0 && row.hour < 24 && row.minute >= 0 && row.minute < 60;
case 'perHour':
+2 -2
View File
@@ -19,10 +19,10 @@
<el-option :label="$t('commons.status.success')" value="Success"></el-option>
<el-option :label="$t('commons.status.failed')" value="Failed"></el-option>
</el-select>
<el-tag type="success" size="large" style="margin-left: 15px">
<el-tag v-if="searchStatus === 'All'" type="success" size="large" style="margin-left: 15px">
{{ $t('commons.status.success') }}: {{ successfulCount }}
</el-tag>
<el-tag type="danger" size="large" style="margin-left: 5px">
<el-tag v-if="searchStatus === 'All'" type="danger" size="large" style="margin-left: 5px">
{{ $t('commons.status.failed') }}: {{ faliedCount }}
</el-tag>
</el-col>
@@ -123,14 +123,14 @@
></el-empty>
</div>
</el-tabs>
<el-button @click="toggleFullscreen" class="fullScreen" icon="FullScreen"></el-button>
<el-button @click="toggleFullscreen" v-if="!mobile" class="fullScreen" icon="FullScreen"></el-button>
<HostDialog ref="dialogRef" @on-conn-terminal="onConnTerminal" @load-host-tree="loadHostTree" />
</div>
</template>
<script setup lang="ts">
import { ref, getCurrentInstance, watch, nextTick } from 'vue';
import { ref, getCurrentInstance, watch, nextTick, computed } from 'vue';
import Terminal from '@/components/terminal/index.vue';
import HostDialog from '@/views/host/terminal/terminal/host-create.vue';
import type Node from 'element-plus/es/components/tree/src/model/node';
@@ -145,15 +145,15 @@ import { GlobalStore } from '@/store';
const dialogRef = ref();
const ctx = getCurrentInstance() as any;
const globalStore = GlobalStore();
const mobile = computed(() => {
return globalStore.isMobile();
});
function toggleFullscreen() {
if (screenfull.isEnabled) {
screenfull.toggle();
}
}
screenfull.on('change', () => {
globalStore.isFullScreen = screenfull.isFullscreen;
});
const localHostID = ref();
@@ -209,6 +209,12 @@ const acceptParams = async () => {
}
}
}
if (!mobile.value) {
screenfull.on('change', () => {
globalStore.isFullScreen = screenfull.isFullscreen;
});
}
};
const cleanTimer = () => {
clearInterval(Number(timer));
+8 -3
View File
@@ -17,6 +17,7 @@ require (
github.com/gin-contrib/gzip v0.0.6
github.com/gin-contrib/i18n v0.0.1
github.com/gin-gonic/gin v1.9.1
github.com/glebarez/sqlite v1.8.0
github.com/go-acme/lego/v4 v4.9.0
github.com/go-gormigrate/gormigrate/v2 v2.0.2
github.com/go-playground/validator/v10 v10.14.0
@@ -55,8 +56,7 @@ require (
gopkg.in/ini.v1 v1.67.0
gopkg.in/square/go-jose.v2 v2.6.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/sqlite v1.4.4
gorm.io/gorm v1.24.5
gorm.io/gorm v1.24.6
)
require (
@@ -112,6 +112,7 @@ require (
github.com/fvbommel/sortorder v1.0.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.21.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
@@ -166,7 +167,6 @@ require (
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/dns v1.1.50 // indirect
@@ -200,6 +200,7 @@ require (
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rs/xid v1.4.0 // indirect
github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002 // indirect
@@ -258,6 +259,10 @@ require (
k8s.io/client-go v0.24.1 // indirect
k8s.io/klog/v2 v2.80.1 // indirect
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect
modernc.org/libc v1.22.3 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.21.1 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
+19 -8
View File
@@ -276,6 +276,10 @@ github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjX
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/glebarez/go-sqlite v1.21.1 h1:7MZyUPh2XTrHS7xNEHQbrhfMZuPSzhkm2A1qgg0y5NY=
github.com/glebarez/go-sqlite v1.21.1/go.mod h1:ISs8MF6yk5cL4n/43rSOmVMGJJjHYr7L2MbZZ5Q4E2E=
github.com/glebarez/sqlite v1.8.0 h1:02X12E2I/4C1n+v90yTqrjRa8yuo7c3KeHI3FRznCvc=
github.com/glebarez/sqlite v1.8.0/go.mod h1:bpET16h1za2KOOMb8+jCp6UBP/iahDpfPQqSaYLTLx8=
github.com/go-acme/lego/v4 v4.9.0 h1:8Hjj44IqRS7cigshMyFQ+0pIZvwgkG/+9A0UnNh7G8A=
github.com/go-acme/lego/v4 v4.9.0/go.mod h1:g3JRUyWS3L/VObpp4bCxzJftKyf/Wba8QrSSnoiqjg4=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -431,6 +435,7 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
@@ -504,7 +509,6 @@ github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLl
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
@@ -592,9 +596,7 @@ github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
@@ -748,6 +750,9 @@ github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdk
github.com/qiniu/go-sdk/v7 v7.14.0 h1:6icihMTKHoKMmeU1mqtIoHUv7c1LrLjYm8wTQaYDqmw=
github.com/qiniu/go-sdk/v7 v7.14.0/go.mod h1:btsaOc8CA3hdVloULfFdDgDc+g4f3TDZEFsDY0BLE+w=
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
@@ -1339,12 +1344,10 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.3.3 h1:jXG9ANrwBc4+bMvBcSl8zCfPBaVoPyBEBshA8dA93X8=
gorm.io/driver/postgres v1.3.7 h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ=
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/driver/sqlite v1.3.2 h1:nWTy4cE52K6nnMhv23wLmur9Y3qWbZvOBz+V4PrGAxg=
gorm.io/driver/sqlserver v1.3.2 h1:yYt8f/xdAKLY7lCCyXxIUEgZ/WsURos3dHrx8MKFGAk=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE=
gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -1368,6 +1371,14 @@ k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2R
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d h1:0Smp/HP1OH4Rvhe+4B8nWGERtlqAGSftbSbbmm45oFs=
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=