feat: 面板设置增加备份账号管理

This commit is contained in:
ssongliu 2022-09-16 18:53:45 +08:00 committed by ssongliu
parent fc3318c8e9
commit f99a2ae656
41 changed files with 1660 additions and 207 deletions

View File

@ -0,0 +1,112 @@
package v1
import (
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) CreateBackup(c *gin.Context) {
var req dto.BackupOperate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := backupService.Create(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) ListBuckets(c *gin.Context) {
var req dto.ForBuckets
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
buckets, err := backupService.GetBuckets(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, buckets)
}
func (b *BaseApi) DeleteBackup(c *gin.Context) {
var req dto.BatchDeleteReq
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := backupService.BatchDelete(req.Ids); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) UpdateBackup(c *gin.Context) {
var req dto.BackupOperate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
id, err := helper.GetParamID(c)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
upMap := make(map[string]interface{})
upMap["bucket"] = req.Bucket
upMap["credential"] = req.Credential
upMap["vars"] = req.Vars
if err := backupService.Update(id, upMap); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) PageBackup(c *gin.Context) {
var req dto.PageInfo
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := global.VALID.Struct(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
total, list, err := backupService.Page(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}

View File

@ -11,6 +11,7 @@ var ApiGroupApp = new(ApiGroup)
var (
authService = service.ServiceGroupApp.AuthService
hostService = service.ServiceGroupApp.HostService
backupService = service.ServiceGroupApp.BackupService
groupService = service.ServiceGroupApp.GroupService
commandService = service.ServiceGroupApp.CommandService
operationService = service.ServiceGroupApp.OperationService

26
backend/app/dto/backup.go Normal file
View File

@ -0,0 +1,26 @@
package dto
import "time"
type BackupOperate struct {
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`
Bucket string `json:"bucket"`
Credential string `json:"credential"`
Vars string `json:"vars" validate:"required"`
}
type BackupInfo struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"createdAt"`
Name string `json:"name"`
Type string `json:"type"`
Bucket string `json:"bucket"`
Vars string `json:"vars"`
}
type ForBuckets struct {
Type string `json:"type" validate:"required"`
Credential string `json:"credential" validate:"required"`
Vars string `json:"vars" validate:"required"`
}

View File

@ -0,0 +1,11 @@
package model
type BackupAccount struct {
BaseModel
Name string `gorm:"type:varchar(64);not null" json:"name"`
Type string `gorm:"type:varchar(64)" json:"type"`
Bucket string `gorm:"type:varchar(256)" json:"bucket"`
Credential string `gorm:"type:varchar(256)" json:"credential"`
Vars string `gorm:"type:longText" json:"vars"`
Status string `gorm:"type:varchar(64)" json:"status"`
}

View File

@ -0,0 +1,57 @@
package repo
import (
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/global"
)
type BackupRepo struct{}
type IBackupRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.BackupAccount, error)
Create(backup *model.BackupAccount) error
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
}
func NewIBackupService() IBackupRepo {
return &BackupRepo{}
}
func (u *BackupRepo) Get(opts ...DBOption) (model.BackupAccount, error) {
var backup model.BackupAccount
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&backup).Error
return backup, err
}
func (u *BackupRepo) Page(page, size int, opts ...DBOption) (int64, []model.BackupAccount, error) {
var ops []model.BackupAccount
db := global.DB.Model(&model.BackupAccount{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&ops).Error
return count, ops, err
}
func (u *BackupRepo) Create(backup *model.BackupAccount) error {
return global.DB.Create(backup).Error
}
func (u *BackupRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.BackupAccount{}).Where("id = ?", id).Updates(vars).Error
}
func (u *BackupRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.BackupAccount{}).Error
}

View File

@ -2,6 +2,7 @@ package repo
type RepoGroup struct {
HostRepo
BackupRepo
GroupRepo
CommandRepo
OperationRepo

View File

@ -0,0 +1,83 @@
package service
import (
"encoding/json"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/utils/cloud_storage"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)
type BackupService struct{}
type IBackupService interface {
Page(search dto.PageInfo) (int64, interface{}, error)
Create(backupDto dto.BackupOperate) error
GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error)
Update(id uint, upMap map[string]interface{}) error
BatchDelete(ids []uint) error
}
func NewIBackupService() IBackupService {
return &BackupService{}
}
func (u *BackupService) Page(search dto.PageInfo) (int64, interface{}, error) {
total, ops, err := backupRepo.Page(search.Page, search.PageSize, commonRepo.WithOrderBy("created_at desc"))
var dtobas []dto.BackupInfo
for _, group := range ops {
var item dto.BackupInfo
if err := copier.Copy(&item, &group); err != nil {
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}
dtobas = append(dtobas, item)
}
return total, dtobas, err
}
func (u *BackupService) Create(backupDto dto.BackupOperate) error {
backup, _ := backupRepo.Get(commonRepo.WithByName(backupDto.Name))
if backup.ID != 0 {
return constant.ErrRecordExist
}
if err := copier.Copy(&backup, &backupDto); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
if err := backupRepo.Create(&backup); err != nil {
return err
}
var backupinfo dto.BackupInfo
if err := copier.Copy(&backupinfo, &backup); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
return nil
}
func (u *BackupService) GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error) {
varMap := make(map[string]interface{})
if err := json.Unmarshal([]byte(backupDto.Vars), &varMap); err != nil {
return nil, err
}
varMap["type"] = backupDto.Type
switch backupDto.Type {
case constant.Sftp:
varMap["password"] = backupDto.Credential
case constant.OSS, constant.S3, constant.MinIo:
varMap["secretKey"] = backupDto.Credential
}
client, err := cloud_storage.NewCloudStorageClient(varMap)
if err != nil {
return nil, err
}
return client.ListBuckets()
}
func (u *BackupService) BatchDelete(ids []uint) error {
return backupRepo.Delete(commonRepo.WithIdsIn(ids))
}
func (u *BackupService) Update(id uint, upMap map[string]interface{}) error {
return backupRepo.Update(id, upMap)
}

View File

@ -5,6 +5,7 @@ import "github.com/1Panel-dev/1Panel/app/repo"
type ServiceGroup struct {
AuthService
HostService
BackupService
GroupService
CommandService
OperationService
@ -16,6 +17,7 @@ var ServiceGroupApp = new(ServiceGroup)
var (
hostRepo = repo.RepoGroupApp.HostRepo
backupRepo = repo.RepoGroupApp.BackupRepo
groupRepo = repo.RepoGroupApp.GroupRepo
commandRepo = repo.RepoGroupApp.CommandRepo
operationRepo = repo.RepoGroupApp.OperationRepo

View File

@ -56,7 +56,7 @@ func (u *OperationService) BatchDelete(ids []uint) error {
}
func filterSensitive(vars string) string {
var Sensitives = []string{"password", "Password", "privateKey"}
var Sensitives = []string{"password", "Password", "credential", "privateKey"}
ops := make(map[string]interface{})
if err := json.Unmarshal([]byte(vars), &ops); err != nil {
return vars

View File

@ -0,0 +1,11 @@
package constant
const (
Valid = "VALID"
DisConnect = "DISCONNECT"
VerifyFailed = "VERIFYFAILED"
S3 = "S3"
OSS = "OSS"
Sftp = "SFTP"
MinIo = "MINIO"
)

View File

@ -22,6 +22,8 @@ var (
ErrRecordNotFound = errors.New("ErrRecordNotFound")
ErrStructTransform = errors.New("ErrStructTransform")
ErrInitialPassword = errors.New("ErrInitialPassword")
ErrNotSupportType = errors.New("ErrNotSupportType")
ErrInvalidParams = errors.New("ErrInvalidParams")
ErrTokenParse = errors.New("ErrTokenParse")

View File

@ -3,6 +3,8 @@ module github.com/1Panel-dev/1Panel
go 1.18
require (
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible
github.com/aws/aws-sdk-go v1.44.99
github.com/dgraph-io/badger/v3 v3.2103.2
github.com/fsnotify/fsnotify v1.5.4
github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6
@ -19,10 +21,12 @@ require (
github.com/jinzhu/copier v0.3.5
github.com/kr/pty v1.1.1
github.com/mholt/archiver/v4 v4.0.0-alpha.7
github.com/minio/minio-go/v7 v7.0.36
github.com/mojocn/base64Captcha v1.3.5
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/nicksnyder/go-i18n/v2 v2.1.2
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.1
github.com/robfig/cron/v3 v3.0.1
github.com/satori/go.uuid v1.2.0
github.com/shirou/gopsutil v3.21.11+incompatible
@ -34,7 +38,7 @@ require (
github.com/swaggo/gin-swagger v1.5.1
github.com/swaggo/swag v1.8.4
github.com/xlzd/gotp v0.0.0-20220817083547-a63b9d03d72f
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
golang.org/x/text v0.3.7
gopkg.in/yaml.v2 v2.4.0
gorm.io/driver/mysql v1.3.5
@ -70,20 +74,26 @@ require (
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/flatbuffers v1.12.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/klauspost/cpuid/v2 v2.1.0 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
@ -91,6 +101,7 @@ require (
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/rs/xid v1.4.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
@ -105,11 +116,12 @@ require (
go.opentelemetry.io/otel v1.0.0 // indirect
go.opentelemetry.io/otel/trace v1.0.0 // indirect
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
golang.org/x/tools v0.1.10 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -48,9 +48,13 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible h1:QoRMR0TCctLDqBCMyOu1eXdZyMw3F7uGA9qPn2J4+R8=
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aws/aws-sdk-go v1.44.99 h1:ITZ9q/fmH+Ksaz2TbyMU2d19vOOWs/hAlt8NbXAieHw=
github.com/aws/aws-sdk-go v1.44.99/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
@ -226,6 +230,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
@ -263,6 +269,10 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
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.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
@ -279,8 +289,13 @@ github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@ -315,6 +330,12 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJK
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mholt/archiver/v4 v4.0.0-alpha.7 h1:xzByj8G8tj0Oq7ZYYU4+ixL/CVb5ruWCm0EZQ1PjOkE=
github.com/mholt/archiver/v4 v4.0.0-alpha.7/go.mod h1:Fs8qUkO74HHaidabihzYephJH8qmGD/nCP6tE5xC9BM=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.36 h1:KPzAl8C6jcRFEUsGUHR6deRivvKATPNZThzi7D9y/sc=
github.com/minio/minio-go/v7 v7.0.36/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@ -352,6 +373,7 @@ github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFu
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -362,6 +384,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
@ -464,8 +488,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -539,9 +563,11 @@ golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -611,8 +637,10 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -627,6 +655,7 @@ golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -785,8 +814,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI=
gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -9,4 +9,5 @@ ErrRecordExist: "Record already exists: {{ .detail }}"
ErrRecordNotFound: "Records not found: {{ .detail }}"
ErrStructTransform: "Type conversion failure: {{ .detail }}"
ErrNotLogin: "User is not Login: {{ .detail }}"
ErrNotSafety: "The login status of the current user is unsafe: {{ .detail }}"
ErrNotSafety: "The login status of the current user is unsafe: {{ .detail }}"
ErrNotSupportType: "The system does not support the current type: {{ .detail }}"

View File

@ -9,4 +9,5 @@ ErrRecordExist: "记录已存在: {{ .detail }}"
ErrRecordNotFound: "记录未能找到: {{ .detail }}"
ErrStructTransform: "类型转换失败: {{ .detail }}"
ErrNotLogin: "用户未登录: {{ .detail }}"
ErrNotSafety: "当前用户登录状态不安全: {{ .detail }}"
ErrNotSafety: "当前用户登录状态不安全: {{ .detail }}"
ErrNotSupportType: "系统暂不支持当前类型: {{ .detail }}"

View File

@ -13,6 +13,7 @@ func Init() {
migrations.AddTableHost,
migrations.AddTableMonitor,
migrations.AddTableSetting,
migrations.AddTableBackupAccount,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View File

@ -119,3 +119,22 @@ var AddTableSetting = &gormigrate.Migration{
return nil
},
}
var AddTableBackupAccount = &gormigrate.Migration{
ID: "20200916-add-table-backup",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.BackupAccount{}); err != nil {
return err
}
item := &model.BackupAccount{
Name: "Default Local",
Type: "LOCAL",
Status: "VALID",
Vars: "{\"dir\":\"/opt/1Panel/backup\"}",
}
if err := tx.Create(item).Error; err != nil {
return err
}
return nil
},
}

View File

@ -45,6 +45,7 @@ func Routers() *gin.Engine {
{
systemRouter.InitBaseRouter(PrivateGroup)
systemRouter.InitHostRouter(PrivateGroup)
systemRouter.InitBackupRouter(PrivateGroup)
systemRouter.InitGroupRouter(PrivateGroup)
systemRouter.InitCommandRouter(PrivateGroup)
systemRouter.InitTerminalRouter(PrivateGroup)

View File

@ -3,6 +3,7 @@ package router
type RouterGroup struct {
BaseRouter
HostRouter
BackupRouter
GroupRouter
CommandRouter
MonitorRouter

View File

@ -0,0 +1,23 @@
package router
import (
v1 "github.com/1Panel-dev/1Panel/app/api/v1"
"github.com/1Panel-dev/1Panel/middleware"
"github.com/gin-gonic/gin"
)
type BackupRouter struct{}
func (s *BackupRouter) InitBackupRouter(Router *gin.RouterGroup) {
baRouter := Router.Group("backups").Use(middleware.JwtAuth()).Use(middleware.SessionAuth())
withRecordRouter := Router.Group("backups").Use(middleware.JwtAuth()).Use(middleware.SessionAuth()).Use(middleware.OperationRecord())
baseApi := v1.ApiGroupApp.BaseApi
{
baRouter.POST("/search", baseApi.PageBackup)
baRouter.POST("/buckets", baseApi.ListBuckets)
withRecordRouter.POST("", baseApi.CreateBackup)
withRecordRouter.POST("/del", baseApi.DeleteBackup)
withRecordRouter.PUT(":id", baseApi.UpdateBackup)
}
}

View File

@ -0,0 +1,158 @@
package client
import (
"context"
"crypto/tls"
"io"
"net/http"
"os"
"strings"
"github.com/1Panel-dev/1Panel/constant"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
type minIoClient struct {
Vars map[string]interface{}
client *minio.Client
}
func NewMinIoClient(vars map[string]interface{}) (*minIoClient, error) {
var endpoint string
var accessKeyID string
var secretAccessKey string
if _, ok := vars["endpoint"]; ok {
endpoint = vars["endpoint"].(string)
} else {
return nil, constant.ErrInvalidParams
}
if _, ok := vars["accessKey"]; ok {
accessKeyID = vars["accessKey"].(string)
} else {
return nil, constant.ErrInvalidParams
}
if _, ok := vars["secretKey"]; ok {
secretAccessKey = vars["secretKey"].(string)
} else {
return nil, constant.ErrInvalidParams
}
ssl := strings.Split(endpoint, ":")[0]
if len(ssl) == 0 || (ssl != "https" && ssl != "http") {
return nil, constant.ErrInvalidParams
}
secure := false
tlsConfig := &tls.Config{}
if ssl == "https" {
secure = true
tlsConfig.InsecureSkipVerify = true
}
var transport http.RoundTripper = &http.Transport{
TLSClientConfig: tlsConfig,
}
client, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
Secure: secure,
Transport: transport,
})
if err != nil {
return nil, err
}
return &minIoClient{
client: client,
Vars: vars,
}, nil
}
func (minIo minIoClient) ListBuckets() ([]interface{}, error) {
buckets, err := minIo.client.ListBuckets(context.Background())
if err != nil {
return nil, err
}
var result []interface{}
for _, bucket := range buckets {
result = append(result, bucket.Name)
}
return result, err
}
func (minIo minIoClient) Exist(path string) (bool, error) {
if _, ok := minIo.Vars["bucket"]; ok {
_, err := minIo.client.GetObject(context.Background(), minIo.Vars["bucket"].(string), path, minio.GetObjectOptions{})
if err != nil {
return true, err
}
return false, nil
} else {
return false, constant.ErrInvalidParams
}
}
func (minIo minIoClient) Delete(path string) (bool, error) {
if _, ok := minIo.Vars["bucket"]; ok {
object, err := minIo.client.GetObject(context.Background(), minIo.Vars["bucket"].(string), path, minio.GetObjectOptions{})
if err != nil {
return false, err
}
info, err := object.Stat()
if err != nil {
return false, err
}
err = minIo.client.RemoveObject(context.Background(), minIo.Vars["bucket"].(string), path, minio.RemoveObjectOptions{
GovernanceBypass: true,
VersionID: info.VersionID,
})
if err != nil {
return false, err
}
return true, nil
} else {
return false, constant.ErrInvalidParams
}
}
func (minIo minIoClient) Upload(src, target string) (bool, error) {
var bucket string
if _, ok := minIo.Vars["bucket"]; ok {
bucket = minIo.Vars["bucket"].(string)
} else {
return false, constant.ErrInvalidParams
}
file, err := os.Open(src)
if err != nil {
return false, err
}
defer file.Close()
fileStat, err := file.Stat()
if err != nil {
return false, err
}
_, err = minIo.client.PutObject(context.Background(), bucket, target, file, fileStat.Size(), minio.PutObjectOptions{ContentType: "application/octet-stream"})
if err != nil {
return false, err
}
return true, nil
}
func (minIo minIoClient) Download(src, target string) (bool, error) {
if _, ok := minIo.Vars["bucket"]; ok {
object, err := minIo.client.GetObject(context.Background(), minIo.Vars["bucket"].(string), src, minio.GetObjectOptions{})
if err != nil {
return false, err
}
localFile, err := os.Create(target)
if err != nil {
return false, err
}
if _, err = io.Copy(localFile, object); err != nil {
return false, err
}
return true, nil
} else {
return false, constant.ErrInvalidParams
}
}

View File

@ -0,0 +1,109 @@
package client
import (
"github.com/1Panel-dev/1Panel/constant"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)
type ossClient struct {
Vars map[string]interface{}
client oss.Client
}
func NewOssClient(vars map[string]interface{}) (*ossClient, error) {
var endpoint string
var accessKey string
var secretKey string
if _, ok := vars["endpoint"]; ok {
endpoint = vars["endpoint"].(string)
} else {
return nil, constant.ErrInvalidParams
}
if _, ok := vars["accessKey"]; ok {
accessKey = vars["accessKey"].(string)
} else {
return nil, constant.ErrInvalidParams
}
if _, ok := vars["secretKey"]; ok {
secretKey = vars["secretKey"].(string)
} else {
return nil, constant.ErrInvalidParams
}
client, err := oss.New(endpoint, accessKey, secretKey)
if err != nil {
return nil, err
}
return &ossClient{
Vars: vars,
client: *client,
}, nil
}
func (oss ossClient) ListBuckets() ([]interface{}, error) {
response, err := oss.client.ListBuckets()
if err != nil {
return nil, err
}
var result []interface{}
for _, bucket := range response.Buckets {
result = append(result, bucket.Name)
}
return result, err
}
func (oss ossClient) Exist(path string) (bool, error) {
bucket, err := oss.GetBucket()
if err != nil {
return false, err
}
return bucket.IsObjectExist(path)
}
func (oss ossClient) Delete(path string) (bool, error) {
bucket, err := oss.GetBucket()
if err != nil {
return false, err
}
err = bucket.DeleteObject(path)
if err != nil {
return false, err
}
return true, nil
}
func (oss ossClient) Upload(src, target string) (bool, error) {
bucket, err := oss.GetBucket()
if err != nil {
return false, err
}
err = bucket.PutObjectFromFile(target, src)
if err != nil {
return false, err
}
return true, nil
}
func (oss ossClient) Download(src, target string) (bool, error) {
bucket, err := oss.GetBucket()
if err != nil {
return false, err
}
err = bucket.GetObjectToFile(src, target)
if err != nil {
return false, err
}
return true, nil
}
func (oss *ossClient) GetBucket() (*oss.Bucket, error) {
if _, ok := oss.Vars["bucket"]; ok {
bucket, err := oss.client.Bucket(oss.Vars["bucket"].(string))
if err != nil {
return nil, err
}
return bucket, nil
} else {
return nil, constant.ErrInvalidParams
}
}

View File

@ -0,0 +1,177 @@
package client
import (
"os"
"github.com/1Panel-dev/1Panel/constant"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
)
type s3Client struct {
Vars map[string]interface{}
Sess session.Session
}
func NewS3Client(vars map[string]interface{}) (*s3Client, error) {
var accessKey string
var secretKey string
var endpoint string
var region string
if _, ok := vars["accessKey"]; ok {
accessKey = vars["accessKey"].(string)
} else {
return nil, constant.ErrInvalidParams
}
if _, ok := vars["secretKey"]; ok {
secretKey = vars["secretKey"].(string)
} else {
return nil, constant.ErrInvalidParams
}
if _, ok := vars["endpoint"]; ok {
endpoint = vars["endpoint"].(string)
} else {
return nil, constant.ErrInvalidParams
}
if _, ok := vars["region"]; ok {
region = vars["region"].(string)
} else {
return nil, constant.ErrInvalidParams
}
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),
})
if err != nil {
return nil, err
}
return &s3Client{
Vars: vars,
Sess: *sess,
}, nil
}
func (s3C s3Client) ListBuckets() ([]interface{}, error) {
var result []interface{}
svc := s3.New(&s3C.Sess)
res, err := svc.ListBuckets(nil)
if err != nil {
return nil, err
}
for _, b := range res.Buckets {
result = append(result, b.Name)
}
return result, nil
}
func (s3C s3Client) Exist(path string) (bool, error) {
bucket, err := s3C.getBucket()
if err != nil {
return false, err
}
svc := s3.New(&s3C.Sess)
_, err = svc.HeadObject(&s3.HeadObjectInput{
Bucket: &bucket,
Key: &path,
})
if err != nil {
if aerr, ok := err.(awserr.RequestFailure); ok {
if aerr.StatusCode() == 404 {
return false, nil
}
} else {
return false, aerr
}
}
return true, nil
}
func (s3C s3Client) Delete(path string) (bool, error) {
bucket, err := s3C.getBucket()
if err != nil {
return false, err
}
svc := s3.New(&s3C.Sess)
_, err = svc.DeleteObject(&s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String(path)})
if err != nil {
return false, err
}
err = svc.WaitUntilObjectNotExists(&s3.HeadObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(path),
})
if err != nil {
return false, err
}
return true, nil
}
func (s3C s3Client) Upload(src, target string) (bool, error) {
bucket, err := s3C.getBucket()
if err != nil {
return false, err
}
file, err := os.Open(src)
if err != nil {
return false, err
}
defer file.Close()
uploader := s3manager.NewUploader(&s3C.Sess)
_, err = uploader.Upload(&s3manager.UploadInput{
Bucket: aws.String(bucket),
Key: aws.String(target),
Body: file,
})
if err != nil {
return false, err
}
return true, nil
}
func (s3C s3Client) Download(src, target string) (bool, error) {
bucket, err := s3C.getBucket()
if err != nil {
return false, err
}
_, err = os.Stat(target)
if err != nil {
if os.IsNotExist(err) {
os.Remove(target)
} else {
return false, err
}
}
file, err := os.Create(target)
if err != nil {
return false, err
}
defer file.Close()
downloader := s3manager.NewDownloader(&s3C.Sess)
_, err = downloader.Download(file,
&s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(src),
})
if err != nil {
os.Remove(target)
return false, err
}
return true, nil
}
func (s3C *s3Client) getBucket() (string, error) {
if _, ok := s3C.Vars["bucket"]; ok {
return s3C.Vars["bucket"].(string), nil
} else {
return "", constant.ErrInvalidParams
}
}

View File

@ -0,0 +1,212 @@
package client
import (
"fmt"
"io/ioutil"
"net"
"os"
"path"
"strconv"
"time"
"github.com/1Panel-dev/1Panel/constant"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
)
type sftpClient struct {
Vars map[string]interface{}
}
func NewSftpClient(vars map[string]interface{}) (*sftpClient, error) {
if _, ok := vars["address"]; !ok {
return nil, constant.ErrInvalidParams
}
if _, ok := vars["port"].(float64); !ok {
return nil, constant.ErrInvalidParams
}
if _, ok := vars["password"]; !ok {
return nil, constant.ErrInvalidParams
}
if _, ok := vars["username"]; !ok {
return nil, constant.ErrInvalidParams
}
return &sftpClient{
Vars: vars,
}, nil
}
func (s sftpClient) Upload(src, target string) (bool, error) {
bucket, err := s.getBucket()
if err != nil {
return false, err
}
port, err := strconv.Atoi(strconv.FormatFloat(s.Vars["port"].(float64), 'G', -1, 64))
if err != nil {
return false, err
}
sftpC, err := connect(s.Vars["username"].(string), s.Vars["password"].(string), s.Vars["address"].(string), port)
if err != nil {
return false, err
}
defer sftpC.Close()
srcFile, err := os.Open(src)
if err != nil {
return false, err
}
defer srcFile.Close()
targetFilePath := bucket + "/" + target
remotePath, _ := path.Split(targetFilePath)
_, err = sftpC.Stat(remotePath)
if err != nil {
if os.IsNotExist(err) {
err = sftpC.MkdirAll(remotePath)
if err != nil {
return false, err
}
} else {
return false, err
}
}
dstFile, err := sftpC.Create(targetFilePath)
if err != nil {
return false, err
}
defer dstFile.Close()
ff, err := ioutil.ReadAll(srcFile)
if err != nil {
return false, err
}
_, _ = dstFile.Write(ff)
return true, nil
}
func (s sftpClient) ListBuckets() ([]interface{}, error) {
var result []interface{}
return result, nil
}
func (s sftpClient) Download(src, target string) (bool, error) {
bucket, err := s.getBucket()
if err != nil {
return false, err
}
port, err := strconv.Atoi(strconv.FormatFloat(s.Vars["port"].(float64), 'G', -1, 64))
if err != nil {
return false, err
}
sftpC, err := connect(s.Vars["username"].(string), s.Vars["password"].(string), s.Vars["address"].(string), port)
if err != nil {
return false, err
}
defer sftpC.Close()
srcFile, err := sftpC.Open(bucket + "/" + src)
if err != nil {
return false, err
}
defer srcFile.Close()
dstFile, err := os.Create(target)
if err != nil {
return false, err
}
defer dstFile.Close()
if _, err = srcFile.WriteTo(dstFile); err != nil {
return false, err
}
return true, err
}
func (s sftpClient) Exist(path string) (bool, error) {
bucket, err := s.getBucket()
if err != nil {
return false, err
}
port, err := strconv.Atoi(strconv.FormatFloat(s.Vars["port"].(float64), 'G', -1, 64))
if err != nil {
return false, err
}
sftpC, err := connect(s.Vars["username"].(string), s.Vars["password"].(string), s.Vars["address"].(string), port)
if err != nil {
return false, err
}
defer sftpC.Close()
srcFile, err := sftpC.Open(bucket + "/" + path)
if err != nil {
if os.IsNotExist(err) {
return false, nil
} else {
return false, err
}
}
defer srcFile.Close()
return true, err
}
func (s sftpClient) Delete(filePath string) (bool, error) {
bucket, err := s.getBucket()
if err != nil {
return false, err
}
port, err := strconv.Atoi(strconv.FormatFloat(s.Vars["port"].(float64), 'G', -1, 64))
if err != nil {
return false, err
}
sftpC, err := connect(s.Vars["username"].(string), s.Vars["password"].(string), s.Vars["address"].(string), port)
if err != nil {
return false, err
}
defer sftpC.Close()
targetFilePath := bucket + "/" + filePath
err = sftpC.Remove(targetFilePath)
if err != nil {
if os.IsNotExist(err) {
return true, nil
} else {
return false, err
}
}
return true, nil
}
func connect(user, password, host string, port int) (*sftp.Client, error) {
var (
auth []ssh.AuthMethod
addr string
clientConfig *ssh.ClientConfig
sshClient *ssh.Client
sftpClient *sftp.Client
err error
)
auth = make([]ssh.AuthMethod, 0)
auth = append(auth, ssh.Password(password))
clientConfig = &ssh.ClientConfig{
User: user,
Auth: auth,
Timeout: 30 * time.Second,
HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
return nil
},
}
addr = fmt.Sprintf("%s:%d", host, port)
if sshClient, err = ssh.Dial("tcp", addr, clientConfig); err != nil {
return nil, err
}
if sftpClient, err = sftp.NewClient(sshClient); err != nil {
return nil, err
}
return sftpClient, nil
}
func (s sftpClient) getBucket() (string, error) {
if _, ok := s.Vars["bucket"]; ok {
return s.Vars["bucket"].(string), nil
} else {
return "", constant.ErrInvalidParams
}
}

View File

@ -0,0 +1,30 @@
package cloud_storage
import (
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/utils/cloud_storage/client"
)
type CloudStorageClient interface {
ListBuckets() ([]interface{}, error)
Exist(path string) (bool, error)
Delete(path string) (bool, error)
Upload(src, target string) (bool, error)
Download(src, target string) (bool, error)
}
func NewCloudStorageClient(vars map[string]interface{}) (CloudStorageClient, error) {
if vars["type"] == constant.S3 {
return client.NewS3Client(vars)
}
if vars["type"] == constant.OSS {
return client.NewOssClient(vars)
}
if vars["type"] == constant.Sftp {
return client.NewSftpClient(vars)
}
if vars["type"] == constant.MinIo {
return client.NewMinIoClient(vars)
}
return nil, constant.ErrNotSupportType
}

View File

@ -0,0 +1,23 @@
export namespace Backup {
export interface BackupInfo {
id: number;
name: string;
type: string;
bucket: string;
vars: string;
varsJson: object;
}
export interface BackupOperate {
id: number;
name: string;
type: string;
bucket: string;
credential: string;
vars: string;
}
export interface ForBucket {
type: string;
credential: string;
vars: string;
}
}

View File

@ -0,0 +1,23 @@
import http from '@/api';
import { Backup } from '../interface/backup';
import { ResPage, ReqPage } from '../interface';
export const getBackupList = (params: ReqPage) => {
return http.post<ResPage<Backup.BackupInfo>>(`/backups/search`, params);
};
export const addBackup = (params: Backup.BackupOperate) => {
return http.post<Backup.BackupOperate>(`/backups`, params);
};
export const editBackup = (params: Backup.BackupOperate) => {
return http.put(`/backups/` + params.id, params);
};
export const deleteBackup = (params: { ids: number[] }) => {
return http.post(`/backups/del`, params);
};
export const listBucket = (params: Backup.ForBucket) => {
return http.post(`/backups/buckets`, params);
};

View File

@ -48,7 +48,7 @@ let rowName = ref('');
let data = ref();
let loading = ref(false);
let paths = ref<string[]>([]);
let req = reactive({ path: '/', expand: true });
let req = reactive({ path: '/', expand: true, page: 1, pageSize: 20 });
const props = defineProps({
path: {

View File

@ -64,6 +64,7 @@ export const Rules: CommonRule = {
trigger: 'change',
},
name: {
required: true,
validator: checkName,
trigger: 'blur',
},

View File

@ -28,6 +28,8 @@ export default {
},
table: {
name: 'Name',
type: 'Type',
status: 'Status',
group: 'Group',
createdAt: 'Creation Time',
date: 'Date',
@ -43,6 +45,7 @@ export default {
loginSuccess: 'Login Success',
requestTimeout: 'The request timed out, please try again later',
operationSuccess: 'Successful operation',
notSupportOperation: 'This operation is not supported',
infoTitle: 'Hint',
sureLogOut: 'Are you sure you want to log out?',
createSuccess: 'Create Success',
@ -65,6 +68,7 @@ export default {
rule: {
username: 'Please enter a username',
password: 'Please enter a password',
rePassword: 'The passwords are inconsistent. Please check and re-enter the password',
requiredInput: 'Please enter the required fields',
requiredSelect: 'Please select the required fields',
commonName: 'Support English, Chinese, numbers, .-_, length 1-30',
@ -72,6 +76,7 @@ export default {
'Please enter a password with more than 8 characters and must contain letters, digits, and special symbols',
commonPassword: 'Please enter a password with more than 6 characters',
email: 'Email format error',
number: 'Please enter the correct number',
ip: 'Please enter the correct IP address',
port: 'Please enter the correct port',
},
@ -102,7 +107,6 @@ export default {
},
menu: {
home: 'Overview',
terminal: 'Terminal',
apps: 'App Store',
website: 'Website',
project: 'Project',
@ -113,12 +117,12 @@ export default {
plan: 'Planned Task',
host: 'Host',
security: 'Security',
systemConfig: 'Panel Settings',
toolbox: 'Toolbox',
monitor: 'Monitor',
operations: 'Operation Records',
files: 'File Management',
monitor: 'Monitor',
terminal: 'Terminal',
settings: 'Setting',
toolbox: 'Toolbox',
operations: 'Operation Records',
},
home: {
welcome: 'Welcome',
@ -130,18 +134,6 @@ export default {
closeAll: 'Close All',
},
header: {
componentSize: 'Component size',
language: 'Language',
theme: 'theme',
layoutConfig: 'Layout config',
primary: 'primary',
darkMode: 'Dark Mode',
greyMode: 'Grey mode',
weakMode: 'Weak mode',
fullScreen: 'Full Screen',
exitFullScreen: 'Exit Full Screen',
personalData: 'Personal Data',
changePassword: 'Change Password',
logout: 'Logout',
},
monitor: {
@ -196,6 +188,8 @@ export default {
hosts: 'Host',
groups: 'Group',
commands: 'Command',
backups: 'Backup Account',
settings: 'Panel Setting',
auth: 'User',
login: ' login',
logout: ' logout',
@ -285,6 +279,19 @@ export default {
oldPassword: 'Original password',
newPassword: 'New password',
retryPassword: 'Confirm password',
backup: 'Backup',
serverDisk: 'Server disks',
backupAccount: 'Backup account',
loadBucket: 'Get bucket',
accountName: 'Account name',
accountKey: 'Account key',
address: 'Address',
port: 'Port',
username: 'Username',
password: 'Password',
path: 'Path',
safe: 'Safe',
panelPort: 'Panel port',
portHelper:
@ -302,9 +309,11 @@ export default {
mfaHelper1: 'Download a MFA verification mobile app such as:',
mfaHelper2: 'Scan the following QR code using the mobile app to obtain the 6-digit verification code',
mfaHelper3: 'Enter six digits from the app',
enableMonitor: 'Enable',
storeDays: 'Expiration time (day)',
cleanMonitor: 'Clearing monitoring records',
message: 'Message',
messageType: 'Message type',
email: 'Email',

View File

@ -26,18 +26,10 @@ export default {
dateStart: '开始日期',
dateEnd: '结束日期',
},
personal: {
about: '关于',
project_url: '项目地址',
issue: '问题反馈',
talk: '参与讨论',
star: '点亮 Star',
version: '版本',
ko_introduction:
'是一个开源的轻量级 Kubernetes 发行版专注于帮助企业规划部署和运营生产级别的 Kubernetes 集群',
},
table: {
name: '名称',
type: '类型',
status: '状态',
group: '组',
createdAt: '创建时间',
date: '时间',
@ -52,6 +44,7 @@ export default {
deleteSuccess: '删除成功',
loginSuccess: '登录成功',
operationSuccess: '操作成功',
notSupportOperation: '不支持的当前操作',
requestTimeout: '请求超时,请稍后重试',
infoTitle: '提示',
sureLogOut: '您是否确认退出登录?',
@ -111,10 +104,6 @@ export default {
},
menu: {
home: '概览',
monitor: '监控',
terminal: '终端',
operations: '操作日志',
files: '文件管理',
apps: '应用商店',
website: '网站',
project: '项目',
@ -125,8 +114,12 @@ export default {
plan: '计划任务',
host: '主机',
security: '安全',
files: '文件管理',
monitor: '监控',
terminal: '终端',
settings: '面板设置',
toolbox: '工具箱',
operations: '操作日志',
},
home: {
welcome: '欢迎使用',
@ -138,18 +131,6 @@ export default {
closeAll: '关闭所有',
},
header: {
componentSize: '组件大小',
language: '国际化',
theme: '全局主题',
layoutConfig: '布局设置',
primary: 'primary',
darkMode: '暗黑模式',
greyMode: '灰色模式',
weakMode: '色弱模式',
fullScreen: '全屏',
exitFullScreen: '退出全屏',
personalData: '个人资料',
changePassword: '修改密码',
logout: '退出登录',
},
monitor: {
@ -204,6 +185,8 @@ export default {
hosts: '主机',
groups: '组',
commands: '快捷命令',
backups: '备份账号',
settings: '面板设置',
auth: '用户',
post: '创建',
put: '更新',
@ -292,6 +275,19 @@ export default {
oldPassword: '原密码',
newPassword: '新密码',
retryPassword: '确认密码',
backup: '备份',
serverDisk: '服务器磁盘',
backupAccount: '备份账号',
loadBucket: '获取桶',
accountName: '账户名称',
accountKey: '账户密钥',
address: '地址',
port: '端口',
username: '用户名',
password: '密码',
path: '路径',
safe: '安全',
panelPort: '面板端口',
portHelper: '建议端口范围8888 - 65535注意有安全组的服务器请提前在安全组放行新端口',
@ -305,9 +301,11 @@ export default {
mfaHelper1: '下载两步验证手机应用 :',
mfaHelper2: '使用手机应用扫描以下二维码获取 6 位验证码',
mfaHelper3: '输入手机应用上的 6 位数字',
enableMonitor: '监控状态',
storeDays: '过期时间 ()',
cleanMonitor: '清空监控记录',
message: '通知',
messageType: '通知方式',
email: '邮箱',
@ -318,5 +316,7 @@ export default {
emailAddr: '邮箱地址',
emailSMTP: '邮箱 SMTP 授权码',
secret: '密钥',
about: '关于',
},
};

View File

@ -82,6 +82,7 @@ const handleClose = () => {
};
const getPath = (path: string) => {
console.log(path);
addForm.newPath = path;
};

View File

@ -1,5 +1,5 @@
<template>
<div style="margin: 20px; margin-left: 20px">
<div style="margin: 20px">
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
<template #toolbar>
<el-button @click="onCreate()">{{ $t('commons.button.create') }}</el-button>

View File

@ -1,5 +1,5 @@
<template>
<el-row style="margin-top: 10px; margin-left: 10px" class="row-box" :gutter="20">
<el-row style="margin-top: 20px" class="row-box" :gutter="20">
<el-col :span="8">
<el-card class="el-card">
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.createConn')" placement="top-start">

View File

@ -1,133 +1,127 @@
<template>
<div>
<el-tabs tab-position="left" @tab-click="handleClick" v-model="routeTab" class="demo-tabs">
<el-tab-pane name="terminal">
<template #label>
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.conn')" placement="right">
<el-icon><Connection /></el-icon>
</el-tooltip>
</template>
<el-tabs
type="card"
class="terminal-tabs"
style="background-color: #efefef"
v-model="terminalValue"
:before-leave="beforeLeave"
@edit="handleTabsRemove"
<div>
<el-card class="topCard">
<el-radio-group @change="handleChange" v-model="activeNames">
<el-radio-button class="topButton" size="large" label="terminal">
{{ $t('menu.terminal') }}
</el-radio-button>
<el-radio-button class="topButton" size="large" label="host">
{{ $t('menu.host') }}
</el-radio-button>
<el-radio-button class="topButton" size="large" label="command">
{{ $t('terminal.quickCommand') }}
</el-radio-button>
</el-radio-group>
</el-card>
</div>
<div v-if="activeNames === 'terminal'">
<el-tabs
type="card"
class="terminal-tabs"
style="background-color: #efefef; margin-top: 20px"
v-model="terminalValue"
:before-leave="beforeLeave"
@edit="handleTabsRemove"
>
<el-tab-pane
:key="item.key"
v-for="item in terminalTabs"
:closable="true"
:label="item.title"
:name="item.key"
>
<el-tab-pane
:key="item.key"
v-for="item in terminalTabs"
:closable="true"
:label="item.title"
:name="item.key"
>
<template #label>
<span class="custom-tabs-label">
<el-icon style="margin-top: 1px" color="#67C23A" v-if="item.status === 'online'">
<circleCheck />
</el-icon>
<el-icon style="margin-top: 1px" color="#F56C6C" v-if="item.status === 'closed'">
<circleClose />
</el-icon>
<span>&nbsp;{{ item.title }}&nbsp;&nbsp;</span>
</span>
</template>
<Terminal
style="height: calc(100vh - 150px); background-color: #000"
:ref="'Ref' + item.key"
:wsID="item.wsID"
:terminalID="item.key"
></Terminal>
<div>
<el-select
v-model="quickCmd"
clearable
filterable
@change="quickInput"
style="width: 25%"
:placeholder="$t('terminal.quickCommand')"
>
<el-option
v-for="cmd in commandList"
:key="cmd.id"
:label="cmd.name + ' [ ' + cmd.command + ' ] '"
:value="cmd.command"
/>
</el-select>
<el-input
:placeholder="$t('terminal.batchInput')"
v-model="batchVal"
@keyup.enter="batchInput"
style="width: 75%"
>
<template #append>
<el-switch v-model="isBatch" class="ml-2" />
</template>
</el-input>
</div>
</el-tab-pane>
<el-tab-pane :closable="false" name="newTabs">
<template #label>
<el-button
v-popover="popoverRef"
style="background-color: #ededed; border: 0"
icon="Plus"
></el-button>
<el-popover ref="popoverRef" width="250px" trigger="hover" virtual-triggering persistent>
<el-button-group style="width: 100%">
<el-button @click="onNewSsh">New ssh</el-button>
<el-button @click="onNewTab">New tab</el-button>
</el-button-group>
<el-input clearable style="margin-top: 5px" v-model="hostfilterInfo">
<template #append><el-button icon="search" /></template>
</el-input>
<el-tree
ref="treeRef"
:expand-on-click-node="false"
node-key="id"
:default-expand-all="true"
:data="hostTree"
:props="defaultProps"
:filter-node-method="filterHost"
>
<template #default="{ node, data }">
<span class="custom-tree-node">
<span>
<a @click="onConn(node, data)">{{ node.label }}</a>
</span>
</span>
</template>
</el-tree>
</el-popover>
</template>
</el-tab-pane>
<div v-if="terminalTabs.length === 0">
<el-empty
style="background-color: #000; height: calc(100vh - 150px)"
:description="$t('terminal.emptyTerminal')"
></el-empty>
<template #label>
<span class="custom-tabs-label">
<el-icon style="margin-top: 1px" color="#67C23A" v-if="item.status === 'online'">
<circleCheck />
</el-icon>
<el-icon style="margin-top: 1px" color="#F56C6C" v-if="item.status === 'closed'">
<circleClose />
</el-icon>
<span>&nbsp;{{ item.title }}&nbsp;&nbsp;</span>
</span>
</template>
<Terminal
style="height: calc(100vh - 178px); background-color: #000"
:ref="'Ref' + item.key"
:wsID="item.wsID"
:terminalID="item.key"
></Terminal>
<div>
<el-select
v-model="quickCmd"
clearable
filterable
@change="quickInput"
style="width: 25%"
:placeholder="$t('terminal.quickCommand')"
>
<el-option
v-for="cmd in commandList"
:key="cmd.id"
:label="cmd.name + ' [ ' + cmd.command + ' ] '"
:value="cmd.command"
/>
</el-select>
<el-input
:placeholder="$t('terminal.batchInput')"
v-model="batchVal"
@keyup.enter="batchInput"
style="width: 75%"
>
<template #append>
<el-switch v-model="isBatch" class="ml-2" />
</template>
</el-input>
</div>
</el-tabs>
<el-button @click="toggleFullscreen" class="fullScreen" icon="FullScreen"></el-button>
</el-tab-pane>
<el-tab-pane name="host">
<template #label>
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.hostList')" placement="right">
<el-icon><Platform /></el-icon>
</el-tooltip>
</template>
<HostTab ref="hostTabRef" />
</el-tab-pane>
<el-tab-pane name="command">
<template #label>
<el-tooltip class="box-item" effect="dark" :content="$t('terminal.quickCmd')" placement="right">
<el-icon><Reading /></el-icon>
</el-tooltip>
</template>
<CommandTab ref="commandTabRef" />
</el-tab-pane>
</el-tabs>
</el-tab-pane>
<el-tab-pane :closable="false" name="newTabs">
<template #label>
<el-button
v-popover="popoverRef"
style="background-color: #ededed; border: 0"
icon="Plus"
></el-button>
<el-popover ref="popoverRef" width="250px" trigger="hover" virtual-triggering persistent>
<el-button-group style="width: 100%">
<el-button @click="onNewSsh">New ssh</el-button>
<el-button @click="onNewTab">New tab</el-button>
</el-button-group>
<el-input clearable style="margin-top: 5px" v-model="hostfilterInfo">
<template #append><el-button icon="search" /></template>
</el-input>
<el-tree
ref="treeRef"
:expand-on-click-node="false"
node-key="id"
:default-expand-all="true"
:data="hostTree"
:props="defaultProps"
:filter-node-method="filterHost"
>
<template #default="{ node, data }">
<span class="custom-tree-node">
<span>
<a @click="onConn(node, data)">{{ node.label }}</a>
</span>
</span>
</template>
</el-tree>
</el-popover>
</template>
</el-tab-pane>
<div v-if="terminalTabs.length === 0">
<el-empty
style="background-color: #000; height: calc(100vh - 150px)"
:description="$t('terminal.emptyTerminal')"
></el-empty>
</div>
</el-tabs>
<el-button @click="toggleFullscreen" class="fullScreen" icon="FullScreen"></el-button>
</div>
<div v-if="activeNames === 'host'"><HostTab ref="hostTabRef" /></div>
<div v-if="activeNames === 'command'"><CommandTab ref="commandTabRef" /></div>
<el-dialog v-model="connVisiable" :title="$t('terminal.addHost')" width="30%">
<el-form ref="hostInfoRef" label-width="100px" label-position="left" :model="hostInfo" :rules="rules">
@ -189,7 +183,7 @@ import screenfull from 'screenfull';
let timer: NodeJS.Timer | null = null;
const routeTab = ref<string>('terminal');
const activeNames = ref<string>('terminal');
const hostTabRef = ref();
const commandTabRef = ref();
@ -248,13 +242,13 @@ function toggleFullscreen() {
screenfull.toggle();
}
}
const handleClick = (tab: any) => {
if (tab.paneName === 'host') {
const handleChange = (tab: any) => {
if (tab === 'host') {
if (ctx) {
ctx.refs[`hostTabRef`] && ctx.refs[`hostTabRef`].onInit();
}
}
if (tab.paneName === 'command') {
if (tab === 'command') {
if (ctx) {
ctx.refs[`commandTabRef`] && ctx.refs[`commandTabRef`].onInit();
}
@ -416,23 +410,23 @@ onBeforeMount(() => {
</script>
<style lang="scss" scoped>
.terminal-tabs {
:deep .el-tabs__header {
:deep(.el-tabs__header) {
padding: 0;
position: relative;
margin: 0 0 3px 0;
}
:deep .el-tabs__nav {
:deep(.el-tabs__nav) {
white-space: nowrap;
position: relative;
transition: transform var(--el-transition-duration);
float: left;
z-index: calc(var(--el-index-normal) + 1);
}
:deep .el-tabs__item {
:deep(.el-tabs__item) {
color: #575758;
padding: 0 0px;
}
:deep .el-tabs__item.is-active {
:deep(.el-tabs__item.is-active) {
color: #ebeef5;
background-color: #575758;
}
@ -454,4 +448,33 @@ onBeforeMount(() => {
.el-tabs--top.el-tabs--card > .el-tabs__header .el-tabs__item:last-child {
padding-right: 0px;
}
.topCard {
--el-card-border-color: var(--el-border-color-light);
--el-card-border-radius: 4px;
--el-card-padding: 0px;
--el-card-bg-color: var(--el-fill-color-blank);
}
.topButton .el-radio-button__inner {
display: inline-block;
line-height: 1;
white-space: nowrap;
vertical-align: middle;
background: var(--el-button-bg-color, var(--el-fill-color-blank));
border: 0;
font-weight: 350;
border-left: 0;
color: var(--el-button-text-color, var(--el-text-color-regular));
text-align: center;
box-sizing: border-box;
outline: 0;
margin: 0;
position: relative;
cursor: pointer;
transition: var(--el-transition-all);
-webkit-user-select: none;
user-select: none;
padding: 8px 15px;
font-size: var(--el-font-size-base);
border-radius: 0;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="demo-collapse">
<div>
<el-card class="topCard">
<el-radio-group v-model="activeNames">
<el-radio-button class="topButton" size="large" label="all">全部</el-radio-button>

View File

@ -1,17 +1,303 @@
<template>
<el-card style="margin-top: 10px">
<el-card style="margin-top: 20px">
<template #header>
<div class="card-header">
<span>备份</span>
<span>{{ $t('setting.backup') }}</span>
</div>
</template>
<el-row>
<el-col :span="1"><br /></el-col>
<el-col :span="8"></el-col>
<el-button type="primary" @click="onCreate">
{{ $t('commons.button.create') }}
</el-button>
<el-row :gutter="20" class="row-box" style="margin-top: 10px; margin-bottom: 20px">
<el-col v-for="item in data" :key="item.id" :span="8">
<el-card class="el-card">
<template #header>
<div class="card-header">
<span style="font-size: 16px; font-weight: 500">
[{{ item.type === 'LOCAL' ? $t('setting.serverDisk') : item.type }}] {{ item.name }}
</span>
<div style="float: right">
<el-button @click="onEdit(item)">{{ $t('commons.button.edit') }}</el-button>
<el-button @click="onBatchDelete(item)">
{{ $t('commons.button.delete') }}
</el-button>
</div>
</div>
</template>
<el-form label-position="left" label-width="130px">
<el-form-item v-if="item.type === 'LOCAL'" label="Dir">
{{ item.varsJson['dir'] }}
</el-form-item>
<el-form-item v-if="hasBucket(item.type)" label="Access Key ID">
{{ item.varsJson['accessKey'] }}
</el-form-item>
<el-form-item v-if="item.type === 'S3'" label="Region">
{{ item.varsJson['region'] }}
</el-form-item>
<el-form-item v-if="hasBucket(item.type)" label="Endpoint">
{{ item.varsJson['endpoint'] }}
</el-form-item>
<el-form-item v-if="hasBucket(item.type)" label="Bucket">
{{ item.bucket }}
</el-form-item>
<el-form-item v-if="item.type === 'SFTP'" :label="$t('setting.address')">
{{ item.varsJson['address'] }}
</el-form-item>
<el-form-item v-if="item.type === 'SFTP'" :label="$t('setting.port')">
{{ item.varsJson['port'] }}
</el-form-item>
<el-form-item v-if="item.type === 'SFTP'" :label="$t('setting.username')">
{{ item.varsJson['username'] }}
</el-form-item>
<el-form-item :label="$t('commons.table.createdAt')">
{{ dateFromat(0, 0, item.createdAt) }}
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
<el-dialog @close="search" v-model="backupVisiable" :title="$t('setting.backupAccount')" width="30%">
<el-form ref="formRef" label-position="left" :model="form" label-width="160px">
<el-form-item :label="$t('commons.table.name')" prop="name" :rules="Rules.name">
<el-input v-model="form.name" :disabled="operation === 'edit'" />
</el-form-item>
<el-form-item :label="$t('commons.table.type')" prop="type" :rules="Rules.requiredSelect">
<el-select style="width: 100%" v-model="form.type" :disabled="operation === 'edit'">
<el-option
v-for="item in typeOptions"
:key="item.label"
:value="item.value"
:label="item.label"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="form.type === 'LOCAL'"
label="Dir"
prop="varsJson['dir']"
:rules="Rules.requiredInput"
>
<el-input v-model="form.varsJson['dir']">
<template #append>
<FileList @choose="loadDir" :dir="true"></FileList>
</template>
</el-input>
</el-form-item>
<el-form-item
v-if="hasBucket(form.type)"
label="Access Key ID"
prop="varsJson.accessKey"
:rules="Rules.requiredInput"
>
<el-input v-model="form.varsJson['accessKey']" />
</el-form-item>
<el-form-item
v-if="hasBucket(form.type)"
label="Secret Access Key"
prop="credential"
:rules="Rules.requiredInput"
>
<el-input show-password v-model="form.credential" />
</el-form-item>
<el-form-item
v-if="form.type === 'S3'"
label="Region"
prop="varsJson.region"
:rules="Rules.requiredInput"
>
<el-input v-model="form.varsJson['region']" />
</el-form-item>
<el-form-item
v-if="hasBucket(form.type)"
label="Endpoint"
prop="varsJson.endpoint"
:rules="Rules.requiredInput"
>
<el-input v-model="form.varsJson['endpoint']" />
</el-form-item>
<el-form-item
v-if="form.type !== '' && hasBucket(form.type)"
label="Bucket"
prop="bucket"
:rules="Rules.requiredSelect"
>
<el-select style="width: 80%" v-model="form.bucket">
<el-option v-for="item in buckets" :key="item" :value="item" />
</el-select>
<el-button style="width: 20%" plain @click="getBuckets">
{{ $t('setting.loadBucket') }}
</el-button>
</el-form-item>
<div v-if="form.type === 'SFTP'">
<el-form-item :label="$t('setting.address')" prop="varsJson.address" :rules="Rules.requiredInput">
<el-input v-model="form.varsJson['address']" />
</el-form-item>
<el-form-item
:label="$t('setting.port')"
prop="varsJson.port"
:rules="[Rules.number, { max: 65535 }]"
>
<el-input v-model.number="form.varsJson['port']" />
</el-form-item>
<el-form-item
:label="$t('setting.username')"
prop="varsJson.username"
:rules="[Rules.requiredInput]"
>
<el-input v-model="form.varsJson['username']" />
</el-form-item>
<el-form-item :label="$t('setting.password')" prop="credential" :rules="[Rules.requiredInput]">
<el-input type="password" show-password v-model="form.credential" />
</el-form-item>
<el-form-item :label="$t('setting.path')" prop="bucket">
<el-input v-model="form.bucket" />
</el-form-item>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="backupVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</el-card>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
onMounted(() => {});
<script setup lang="ts">
import { dateFromat } from '@/utils/util';
import { onMounted, reactive, ref } from 'vue';
import { getBackupList, addBackup, editBackup, listBucket, deleteBackup } from '@/api/modules/backup';
import { Backup } from '@/api/interface/backup';
import i18n from '@/lang';
import { Rules } from '@/global/form-rules';
import { ElForm, ElMessage } from 'element-plus';
import { useDeleteData } from '@/hooks/use-delete-data';
import FileList from '@/components/file-list/index.vue';
const data = ref();
const selects = ref<any>([]);
const backupVisiable = ref<boolean>(false);
const operation = ref<string>('create');
const paginationConfig = reactive({
currentPage: 1,
pageSize: 5,
total: 0,
});
const backSearch = reactive({
page: 1,
pageSize: 5,
});
const form = reactive({
id: 0,
name: '',
type: 'LOCAL',
bucket: '',
credential: '',
vars: '',
varsJson: {},
});
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const typeOptions = ref([
{ label: i18n.global.t('setting.serverDisk'), value: 'LOCAL' },
{ label: 'OSS', value: 'OSS' },
{ label: 'S3', value: 'S3' },
{ label: 'SFTP', value: 'SFTP' },
{ label: 'MINIO', value: 'MINIO' },
]);
const buckets = ref();
const search = async () => {
backSearch.page = paginationConfig.currentPage;
backSearch.pageSize = paginationConfig.pageSize;
const res = await getBackupList(backSearch);
data.value = res.data.items;
for (const bac of data.value) {
bac.varsJson = JSON.parse(bac.vars);
}
paginationConfig.total = res.data.total;
};
const onCreate = () => {
operation.value = 'create';
form.id = 0;
form.name = '';
form.type = 'LOCAL';
form.bucket = '';
form.credential = '';
form.vars = '';
form.varsJson = {};
backupVisiable.value = true;
};
const onBatchDelete = async (row: Backup.BackupInfo | null) => {
let ids: Array<number> = [];
if (row === null) {
selects.value.forEach((item: Backup.BackupInfo) => {
ids.push(item.id);
});
} else {
ids.push(row.id);
}
await useDeleteData(deleteBackup, { ids: ids }, 'commons.msg.delete', true);
search();
restForm();
};
const onEdit = (row: Backup.BackupInfo) => {
restForm();
form.id = row.id;
form.name = row.name;
form.type = row.type;
form.bucket = row.bucket;
form.varsJson = JSON.parse(row.vars);
operation.value = 'edit';
backupVisiable.value = true;
};
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
form.vars = JSON.stringify(form.varsJson);
if (form.id !== 0 && operation.value === 'edit') {
await editBackup(form);
} else if (form.id === 0 && operation.value === 'create') {
await addBackup(form);
} else {
ElMessage.success(i18n.global.t('commons.msg.notSupportOperation'));
return;
}
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
restForm();
search();
backupVisiable.value = false;
});
};
function hasBucket(val: string) {
return val === 'OSS' || val === 'S3' || val === 'MINIO';
}
function restForm() {
if (formRef.value) {
formRef.value.resetFields();
}
}
const getBuckets = async () => {
const res = await listBucket({ type: form.type, vars: JSON.stringify(form.varsJson), credential: form.credential });
buckets.value = res.data;
};
const loadDir = async (path: string) => {
console.log(path);
form.varsJson['dir'] = path;
};
onMounted(() => {
search();
});
</script>

View File

@ -1,6 +1,6 @@
<template>
<el-form :model="mesForm" label-position="left" label-width="160px">
<el-card style="margin-top: 10px">
<el-card style="margin-top: 20px">
<template #header>
<div class="card-header">
<span>{{ $t('setting.message') }}</span>

View File

@ -1,6 +1,6 @@
<template>
<el-form :model="form" ref="panelFormRef" label-position="left" label-width="160px">
<el-card style="margin-top: 10px">
<el-card style="margin-top: 20px">
<template #header>
<div class="card-header">
<span>{{ $t('menu.monitor') }}</span>

View File

@ -178,7 +178,10 @@ type FormInstance = InstanceType<typeof ElForm>;
const passFormRef = ref<FormInstance>();
const passRules = reactive({
oldPassword: [Rules.requiredInput],
newPassword: [Rules.requiredInput, { min: 6, message: i18n.global.t('commons.rule.passwordLen'), trigger: 'blur' }],
newPassword: [
Rules.requiredInput,
{ min: 6, message: i18n.global.t('commons.rule.commonPassword'), trigger: 'blur' },
],
newPasswordComplexity: [Rules.password],
retryPassword: [Rules.requiredInput, { validator: checkPassword, trigger: 'blur' }],
});

View File

@ -135,7 +135,7 @@
<li>{{ $t('setting.mfaHelper3') }}</li>
<el-input v-model="mfaCode"></el-input>
<div style="margin-top: 10px; margin-bottom: 10px; float: right">
<el-button @click="form.settingInfo.mfaStatus = 'disable'">
<el-button @click="onCancelMfaBind">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button @click="onBind">{{ $t('commons.button.saveAndEnable') }}</el-button>
@ -174,7 +174,6 @@ import i18n from '@/lang';
import { Rules } from '@/global/form-rules';
import { dateFromat } from '@/utils/util';
// const emit = defineEmits(['on-save']);
const emit = defineEmits<{ (e: 'on-save', formEl: FormInstance | undefined, key: string, val: any): void }>();
interface Props {
@ -227,6 +226,11 @@ const onBind = async () => {
isMFAShow.value = false;
};
const onCancelMfaBind = async () => {
form.settingInfo.mfaStatus = 'disable';
isMFAShow.value = false;
};
const submitTimeout = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {