Browse Source

feat: 增加从快照恢复功能

feat: 增加升级备份与恢复
pull/104/head
ssongliu 2 years ago committed by ssongliu
parent
commit
e7af9de9ed
  1. 31
      backend/app.yaml
  2. 89
      backend/app/api/v1/snapshot.go
  3. 34
      backend/app/api/v1/upgrade.go
  4. 31
      backend/app/dto/setting.go
  5. 2
      backend/app/model/cronjob.go
  6. 10
      backend/app/model/snapshot.go
  7. 15
      backend/app/repo/common.go
  8. 11
      backend/app/repo/cronjob.go
  9. 25
      backend/app/repo/snapshot.go
  10. 2
      backend/app/service/cornjob.go
  11. 1
      backend/app/service/cronjob_helper.go
  12. 798
      backend/app/service/snapshot.go
  13. 56
      backend/app/service/snapshot_test.go
  14. 5
      backend/configs/cache.go
  15. 5
      backend/configs/config.go
  16. 5
      backend/configs/csrf.go
  17. 3
      backend/configs/system.go
  18. 2
      backend/constant/container.go
  19. 9
      backend/init/cache/cache.go
  20. 2
      backend/init/migration/migrate.go
  21. 7
      backend/init/migration/migrations/init.go
  22. 29
      backend/middleware/csrf.go
  23. 11
      backend/router/ro_setting.go
  24. 5
      backend/server/server.go
  25. 339
      cmd/server/docs/docs.go
  26. 339
      cmd/server/docs/swagger.json
  27. 217
      cmd/server/docs/swagger.yaml
  28. 14
      frontend/package-lock.json
  29. 1
      frontend/package.json
  30. 25
      frontend/src/api/interface/setting.ts
  31. 14
      frontend/src/api/modules/setting.ts
  32. 23
      frontend/src/lang/modules/en.ts
  33. 27
      frontend/src/lang/modules/zh.ts
  34. 53
      frontend/src/views/setting/about/index.vue
  35. 1
      frontend/src/views/setting/index.vue
  36. 8
      frontend/src/views/setting/safe/index.vue
  37. 137
      frontend/src/views/setting/snapshot/index.vue
  38. 242
      frontend/src/views/setting/snapshot/status/index.vue
  39. 4
      go.mod
  40. 8
      go.sum
  41. 15
      node_modules/.package-lock.json
  42. 21
      node_modules/md-editor-v3/LICENSE
  43. 779
      node_modules/md-editor-v3/README.md
  44. 356
      node_modules/md-editor-v3/lib/MdEditor/Editor.d.ts
  45. 26
      node_modules/md-editor-v3/lib/MdEditor/config.d.ts
  46. 39
      node_modules/md-editor-v3/lib/MdEditor/extensions/DropdownToolbar.d.ts
  47. 77
      node_modules/md-editor-v3/lib/MdEditor/extensions/MdCatalog/CatalogLink.d.ts
  48. 111
      node_modules/md-editor-v3/lib/MdEditor/extensions/MdCatalog/index.d.ts
  49. 96
      node_modules/md-editor-v3/lib/MdEditor/extensions/ModalToolbar.d.ts
  50. 27
      node_modules/md-editor-v3/lib/MdEditor/extensions/NormalToolbar.d.ts
  51. 811
      node_modules/md-editor-v3/lib/MdEditor/index.d.ts
  52. 328
      node_modules/md-editor-v3/lib/MdEditor/type.d.ts
  53. 7079
      node_modules/md-editor-v3/lib/md-editor-v3.es.js
  54. 126
      node_modules/md-editor-v3/lib/md-editor-v3.umd.js
  55. 1
      node_modules/md-editor-v3/lib/style.css
  56. 124
      node_modules/md-editor-v3/package.json

31
backend/app.yaml

@ -1,41 +1,38 @@
base_dir: /opt
system:
port: 9999
db_type: sqlite
level: debug
data_dir: /opt/1Panel/data
app_oss: "https://1panel.oss-cn-hangzhou.aliyuncs.com/apps.json"
data_dir: ${base_dir}/1Panel/data
cache: ${base_dir}/1Panel/data/cache
backup: ${base_dir}/1Panel/data/backup
app_oss: "https://1panel.oss-cn-hangzhou.aliyuncs.com/apps/list.json"
sqlite:
path: /opt/1Panel/data/db
path: ${base_dir}/1Panel/data/db
db_file: 1Panel.db
log:
level: info
level: debug
time_zone: Asia/Shanghai
path: /opt/1Panel/log
path: ${base_dir}/1Panel/log
log_name: 1Panel
log_suffix: .log
log_backup: 10 #最大日志保留个数
cache:
path: /opt/1Panel/data/cache
log_backup: 10
# 跨域配置
cors:
mode: whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝
mode: whitelist
whitelist:
- allow-origin: example1.com
allow-headers: content-type
allow-methods: GET, POST
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
allow-credentials: true # 布尔值
allow-credentials: true
- allow-origin: example2.com
allow-headers: content-type
allow-methods: GET, POST
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
allow-credentials: true # 布尔值
allow-credentials: true
# 加密设置
encrypt:
key: 1Panel123@2022!!
key: 1Panel_key@2023!

89
backend/app/api/v1/snapshot.go

@ -9,14 +9,14 @@ import (
)
// @Tags System Setting
// @Summary Create snapshot
// @Summary Create system backup
// @Description 创建系统快照
// @Accept json
// @Param request body dto.SnapshotCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /settings/snapshot [post]
// @x-panel-log {"bodyKeys":["name", "description"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"创建系统快照 [name][description]","formatEN":"Create system snapshot [name][description]"}
// @x-panel-log {"bodyKeys":["from", "description"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"创建系统快照 [description] 到 [from]","formatEN":"Create system backup [description] to [from]"}
func (b *BaseApi) CreateSnapshot(c *gin.Context) {
var req dto.SnapshotCreate
if err := c.ShouldBindJSON(&req); err != nil {
@ -27,7 +27,7 @@ func (b *BaseApi) CreateSnapshot(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := snapshotService.Create(req); err != nil {
if err := snapshotService.SnapshotCreate(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
@ -41,7 +41,7 @@ func (b *BaseApi) CreateSnapshot(c *gin.Context) {
// @Param request body dto.PageInfo true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Router /websites/acme/search [post]
// @Router /settings/snapshot/search [post]
func (b *BaseApi) SearchSnapshot(c *gin.Context) {
var req dto.PageInfo
if err := c.ShouldBindJSON(&req); err != nil {
@ -58,3 +58,84 @@ func (b *BaseApi) SearchSnapshot(c *gin.Context) {
Items: accounts,
})
}
// @Tags System Setting
// @Summary Recover system backup
// @Description 从系统快照恢复
// @Accept json
// @Param request body dto.SnapshotRecover true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /settings/snapshot/recover [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFuntions":[{"input_colume":"id","input_value":"id","isList":false,"db":"snapshots","output_colume":"name","output_value":"name"}],"formatZH":"从系统快照 [name] 恢复","formatEN":"Recover from system backup [name]"}
func (b *BaseApi) RecoverSnapshot(c *gin.Context) {
var req dto.SnapshotRecover
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 := snapshotService.SnapshotRecover(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags System Setting
// @Summary Rollback system backup
// @Description 从系统快照回滚
// @Accept json
// @Param request body dto.SnapshotRecover true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /settings/snapshot/rollback [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFuntions":[{"input_colume":"id","input_value":"id","isList":false,"db":"snapshots","output_colume":"name","output_value":"name"}],"formatZH":"从系统快照 [name] 回滚","formatEN":"Rollback from system backup [name]"}
func (b *BaseApi) RollbackSnapshot(c *gin.Context) {
var req dto.SnapshotRecover
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 := snapshotService.SnapshotRollback(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags System Setting
// @Summary Delete system backup
// @Description 删除系统快照
// @Accept json
// @Param request body dto.BatchDeleteReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /settings/snapshot/del [post]
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFuntions":[{"input_colume":"id","input_value":"ids","isList":true,"db":"snapshots","output_colume":"name","output_value":"name"}],"formatZH":"删除系统快照 [name]","formatEN":"Delete system backup [name]"}
func (b *BaseApi) DeleteSnapshot(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 := snapshotService.Delete(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

34
backend/app/api/v1/upgrade.go

@ -0,0 +1,34 @@
package v1
import (
"context"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/gin-gonic/gin"
"github.com/google/go-github/github"
)
// @Tags System Setting
// @Summary Load upgrade info
// @Description 加载系统更新信息
// @Success 200 {object} dto.UpgradeInfo
// @Security ApiKeyAuth
// @Router /settings/upgrade [get]
func (b *BaseApi) GetUpgradeInfo(c *gin.Context) {
client := github.NewClient(nil)
stats, _, err := client.Repositories.GetLatestRelease(context.Background(), "KubeOperator", "KubeOperator")
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
info := dto.UpgradeInfo{
NewVersion: string(*stats.Name),
Tag: string(*stats.TagName),
ReleaseNote: string(*stats.Body),
CreatedAt: github.Timestamp(*stats.CreatedAt).Format("2006-01-02 15:04:05"),
PublishedAt: github.Timestamp(*stats.PublishedAt).Format("2006-01-02 15:04:05"),
}
helper.SuccessWithData(c, info)
}

31
backend/app/dto/setting.go

@ -3,8 +3,9 @@ package dto
import "time"
type SettingInfo struct {
UserName string `json:"userName"`
Email string `json:"email"`
UserName string `json:"userName"`
Email string `json:"email"`
SystemVersion string `json:"systemVersion"`
SessionTimeout string `json:"sessionTimeout"`
LocalTime string `json:"localTime"`
@ -41,15 +42,37 @@ type PasswordUpdate struct {
}
type SnapshotCreate struct {
BackupType string `json:"backupType" validate:"required,oneof=OSS S3 SFTP MINIO"`
From string `json:"from" validate:"required,oneof=OSS S3 SFTP MINIO"`
Description string `json:"description"`
}
type SnapshotRecover struct {
IsNew bool `json:"isNew"`
ReDownload bool `json:"reDownload"`
ID uint `json:"id" validate:"required"`
}
type SnapshotInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
BackupType string `json:"backupType"`
From string `json:"from"`
Status string `json:"status"`
Message string `json:"message"`
CreatedAt time.Time `json:"createdAt"`
Version string `json:"version"`
InterruptStep string `json:"interruptStep"`
RecoverStatus string `json:"recoverStatus"`
RecoverMessage string `json:"recoverMessage"`
LastRecoveredAt string `json:"lastRecoveredAt"`
RollbackStatus string `json:"rollbackStatus"`
RollbackMessage string `json:"rollbackMessage"`
LastRollbackedAt string `json:"lastRollbackedAt"`
}
type UpgradeInfo struct {
NewVersion string `json:"newVersion"`
Tag string `json:"tag"`
ReleaseNote string `json:"releaseNote"`
CreatedAt string `json:"createdAt"`
PublishedAt string `json:"publishedAt"`
}

2
backend/app/model/cronjob.go

@ -33,7 +33,7 @@ type Cronjob struct {
type JobRecords struct {
BaseModel
CronjobID uint `gorm:"type:varchar(64);not null" json:"cronjobID"`
CronjobID uint `gorm:"type:decimal" json:"cronjobID"`
StartTime time.Time `gorm:"type:datetime" json:"startTime"`
Interval float64 `gorm:"type:float" json:"interval"`
Records string `gorm:"longtext" json:"records"`

10
backend/app/model/snapshot.go

@ -4,8 +4,16 @@ type Snapshot struct {
BaseModel
Name string `json:"name" gorm:"type:varchar(64);not null;unique"`
Description string `json:"description" gorm:"type:varchar(256)"`
BackupType string `json:"backupType" gorm:"type:varchar(64)"`
From string `json:"from"`
Status string `json:"status" gorm:"type:varchar(64)"`
Message string `json:"message" gorm:"type:varchar(256)"`
Version string `json:"version" gorm:"type:varchar(256)"`
InterruptStep string `json:"interruptStep" gorm:"type:varchar(64)"`
RecoverStatus string `json:"recoverStatus" gorm:"type:varchar(64)"`
RecoverMessage string `json:"recoverMessage" gorm:"type:varchar(256)"`
LastRecoveredAt string `json:"lastRecoveredAt" gorm:"type:varchar(64)"`
RollbackStatus string `json:"rollbackStatus" gorm:"type:varchar(64)"`
RollbackMessage string `json:"rollbackMessage" gorm:"type:varchar(256)"`
LastRollbackedAt string `json:"lastRollbackedAt" gorm:"type:varchar(64)"`
}

15
backend/app/repo/common.go

@ -2,6 +2,7 @@ package repo
import (
"context"
"time"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
@ -17,6 +18,8 @@ type ICommonRepo interface {
WithOrderBy(orderStr string) DBOption
WithLikeName(name string) DBOption
WithIdsIn(ids []uint) DBOption
WithByDate(startTime, endTime time.Time) DBOption
WithByStartDate(startTime time.Time) DBOption
}
type CommonRepo struct{}
@ -33,6 +36,18 @@ func (c *CommonRepo) WithByName(name string) DBOption {
}
}
func (c *CommonRepo) WithByDate(startTime, endTime time.Time) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("start_time > ? AND start_time < ?", startTime, endTime)
}
}
func (c *CommonRepo) WithByStartDate(startTime time.Time) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("start_time < ?", startTime)
}
}
func (c *CommonRepo) WithByType(tp string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("type = ?", tp)

11
backend/app/repo/cronjob.go

@ -19,7 +19,6 @@ type ICronjobRepo interface {
List(opts ...DBOption) ([]model.Cronjob, error)
Page(limit, offset int, opts ...DBOption) (int64, []model.Cronjob, error)
Create(cronjob *model.Cronjob) error
WithByDate(startTime, endTime time.Time) DBOption
WithByJobID(id int) DBOption
Save(id uint, cronjob model.Cronjob) error
Update(id uint, vars map[string]interface{}) error
@ -107,16 +106,6 @@ func (u *CronjobRepo) Create(cronjob *model.Cronjob) error {
return global.DB.Create(cronjob).Error
}
func (c *CronjobRepo) WithByDate(startTime, endTime time.Time) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("start_time > ? AND start_time < ?", startTime, endTime)
}
}
func (c *CronjobRepo) WithByStartDate(startTime time.Time) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("start_time < ?", startTime)
}
}
func (c *CronjobRepo) WithByJobID(id int) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("cronjob_id = ?", id)

25
backend/app/repo/snapshot.go

@ -7,9 +7,10 @@ import (
type ISnapshotRepo interface {
Get(opts ...DBOption) (model.Snapshot, error)
Page(limit, offset int, opts ...DBOption) (int64, []model.Snapshot, error)
Create(snapshot *model.Snapshot) error
GetList(opts ...DBOption) ([]model.Snapshot, error)
Create(snap *model.Snapshot) error
Update(id uint, vars map[string]interface{}) error
Page(limit, offset int, opts ...DBOption) (int64, []model.Snapshot, error)
Delete(opts ...DBOption) error
}
@ -20,13 +21,23 @@ func NewISnapshotRepo() ISnapshotRepo {
type SnapshotRepo struct{}
func (u *SnapshotRepo) Get(opts ...DBOption) (model.Snapshot, error) {
var snapshot model.Snapshot
var Snapshot model.Snapshot
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&snapshot).Error
return snapshot, err
err := db.First(&Snapshot).Error
return Snapshot, err
}
func (u *SnapshotRepo) GetList(opts ...DBOption) ([]model.Snapshot, error) {
var snaps []model.Snapshot
db := global.DB.Model(&model.Snapshot{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&snaps).Error
return snaps, err
}
func (u *SnapshotRepo) Page(page, size int, opts ...DBOption) (int64, []model.Snapshot, error) {
@ -41,8 +52,8 @@ func (u *SnapshotRepo) Page(page, size int, opts ...DBOption) (int64, []model.Sn
return count, users, err
}
func (u *SnapshotRepo) Create(snapshot *model.Snapshot) error {
return global.DB.Create(snapshot).Error
func (u *SnapshotRepo) Create(Snapshot *model.Snapshot) error {
return global.DB.Create(Snapshot).Error
}
func (u *SnapshotRepo) Update(id uint, vars map[string]interface{}) error {

2
backend/app/service/cornjob.go

@ -67,7 +67,7 @@ func (u *CronjobService) SearchRecords(search dto.SearchRecord) (int64, interfac
search.PageSize,
commonRepo.WithByStatus(search.Status),
cronjobRepo.WithByJobID(search.CronjobID),
cronjobRepo.WithByDate(search.StartTime, search.EndTime))
commonRepo.WithByDate(search.StartTime, search.EndTime))
var dtoCronjobs []dto.Record
for _, record := range records {
var item dto.Record

1
backend/app/service/cronjob_helper.go

@ -235,6 +235,7 @@ func handleTar(sourceDir, targetDir, name, exclusionRules string) error {
}
cmd := exec.Command("tar", exStr...)
stdout, err := cmd.CombinedOutput()
fmt.Println(string(stdout))
if err != nil {
return errors.New(string(stdout))
}

798
backend/app/service/snapshot.go

@ -2,8 +2,13 @@ package service
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
@ -21,7 +26,12 @@ type SnapshotService struct{}
type ISnapshotService interface {
SearchWithPage(req dto.PageInfo) (int64, interface{}, error)
Create(req dto.SnapshotCreate) error
SnapshotCreate(req dto.SnapshotCreate) error
SnapshotRecover(req dto.SnapshotRecover) error
SnapshotRollback(req dto.SnapshotRecover) error
Delete(req dto.BatchDeleteReq) error
readFromJson(path string) (SnapshotJson, error)
}
func NewISnapshotService() ISnapshotService {
@ -29,11 +39,11 @@ func NewISnapshotService() ISnapshotService {
}
func (u *SnapshotService) SearchWithPage(req dto.PageInfo) (int64, interface{}, error) {
total, snapshots, err := snapshotRepo.Page(req.Page, req.PageSize)
total, systemBackups, err := snapshotRepo.Page(req.Page, req.PageSize)
var dtoSnap []dto.SnapshotInfo
for _, snapshot := range snapshots {
for _, systemBackup := range systemBackups {
var item dto.SnapshotInfo
if err := copier.Copy(&item, &snapshot); err != nil {
if err := copier.Copy(&item, &systemBackup); err != nil {
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}
dtoSnap = append(dtoSnap, item)
@ -41,12 +51,24 @@ func (u *SnapshotService) SearchWithPage(req dto.PageInfo) (int64, interface{},
return total, dtoSnap, err
}
func (u *SnapshotService) Create(req dto.SnapshotCreate) error {
type SnapshotJson struct {
OldDockerDataDir string `json:"oldDockerDataDir"`
OldBackupDataDir string `json:"oldDackupDataDir"`
OldPanelDataDir string `json:"oldPanelDataDir"`
DockerDataDir string `json:"dockerDataDir"`
BackupDataDir string `json:"backupDataDir"`
PanelDataDir string `json:"panelDataDir"`
LiveRestoreEnabled bool `json:"liveRestoreEnabled"`
}
func (u *SnapshotService) SnapshotCreate(req dto.SnapshotCreate) error {
global.LOG.Info("start to create snapshot now")
localDir, err := loadLocalDir()
if err != nil {
return err
}
backup, err := backupRepo.Get(commonRepo.WithByType(req.BackupType))
backup, err := backupRepo.Get(commonRepo.WithByType(req.From))
if err != nil {
return err
}
@ -56,95 +78,755 @@ func (u *SnapshotService) Create(req dto.SnapshotCreate) error {
}
timeNow := time.Now().Format("20060102150405")
rootDir := fmt.Sprintf("/tmp/songliu/1panel_backup_%s", timeNow)
rootDir := fmt.Sprintf("%s/system/1panel_snapshot_%s", localDir, timeNow)
backupPanelDir := fmt.Sprintf("%s/1panel", rootDir)
_ = os.MkdirAll(backupPanelDir, os.ModePerm)
backupDockerDir := fmt.Sprintf("%s/docker", rootDir)
_ = os.MkdirAll(backupDockerDir, os.ModePerm)
defer func() {
_, _ = cmd.Exec("systemctl start docker")
versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion"))
snap := model.Snapshot{
Name: "1panel_snapshot_" + timeNow,
Description: req.Description,
From: req.From,
Version: versionItem.Value,
Status: constant.StatusWaiting,
}
_ = snapshotRepo.Create(&snap)
go func() {
defer func() {
global.LOG.Info("zhengque zoudao le zheli")
_ = os.RemoveAll(rootDir)
}()
fileOp := files.NewFileOp()
dockerDataDir, liveRestoreStatus, err := u.loadDockerDataDir()
if err != nil {
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
_, _ = cmd.Exec("systemctl stop docker")
if err := u.handleDockerDatas(fileOp, "snapshot", dockerDataDir, backupDockerDir); err != nil {
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
if err := u.handleDaemonJson(fileOp, "snapshot", "", backupDockerDir); err != nil {
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
_, _ = cmd.Exec("systemctl restart docker")
if err := u.handlePanelBinary(fileOp, "snapshot", "", backupPanelDir+"/1panel"); err != nil {
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
if err := u.handlePanelctlBinary(fileOp, "snapshot", "", backupPanelDir+"/1pctl"); err != nil {
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
if err := u.handlePanelService(fileOp, "snapshot", "", backupPanelDir+"/1panel.service"); err != nil {
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
if err := u.handleBackupDatas(fileOp, "snapshot", localDir, backupPanelDir); err != nil {
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
if err := u.handlePanelDatas(fileOp, "snapshot", global.CONF.BaseDir+"/1Panel", backupPanelDir, localDir, dockerDataDir); err != nil {
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
snapJson := SnapshotJson{DockerDataDir: dockerDataDir, BackupDataDir: localDir, PanelDataDir: global.CONF.BaseDir + "/1Panel", LiveRestoreEnabled: liveRestoreStatus}
if err := u.saveJson(snapJson, rootDir); err != nil {
updateSnapshotStatus(snap.ID, constant.StatusFailed, fmt.Sprintf("save snapshot json failed, err: %v", err))
return
}
if err := fileOp.Compress([]string{rootDir}, fmt.Sprintf("%s/system", localDir), fmt.Sprintf("1panel_snapshot_%s.tar.gz", timeNow), files.TarGz); err != nil {
updateSnapshotStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
global.LOG.Infof("start to upload snapshot to %s, please wait", backup.Type)
localPath := fmt.Sprintf("%s/system/1panel_snapshot_%s.tar.gz", localDir, timeNow)
if ok, err := backupAccont.Upload(localPath, fmt.Sprintf("system_snapshot/1panel_snapshot_%s.tar.gz", timeNow)); err != nil || !ok {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
global.LOG.Errorf("upload snapshot to %s failed, err: %v", backup.Type, err)
return
}
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
_ = os.RemoveAll(rootDir)
_ = os.RemoveAll(fmt.Sprintf("%s/system/1panel_snapshot_%s.tar.gz", localDir, timeNow))
updateSnapshotStatus(snap.ID, constant.StatusSuccess, "")
global.LOG.Infof("upload snapshot to %s success", backup.Type)
}()
return nil
}
fileOp := files.NewFileOp()
if err := fileOp.Compress([]string{localDir}, backupPanelDir, "1panel_backup.tar.gz", files.TarGz); err != nil {
global.LOG.Errorf("snapshot backup 1panel backup datas %s failed, err: %v", localDir, err)
func (u *SnapshotService) SnapshotRecover(req dto.SnapshotRecover) error {
global.LOG.Info("start to recvover panel by snapshot now")
snap, err := snapshotRepo.Get(commonRepo.WithByID(req.ID))
if err != nil {
return err
}
client, err := docker.NewDockerClient()
if !req.IsNew && len(snap.InterruptStep) != 0 && len(snap.RollbackStatus) != 0 {
return fmt.Errorf("the snapshot has been rolled back and cannot be restored again")
}
isReTry := false
if len(snap.InterruptStep) != 0 && !req.IsNew {
isReTry = true
}
backup, err := backupRepo.Get(commonRepo.WithByType(snap.From))
if err != nil {
return err
}
ctx := context.Background()
info, err := client.Info(ctx)
client, err := NewIBackupService().NewClient(&backup)
if err != nil {
return err
}
dataDir := info.DockerRootDir
stdout, err := cmd.Exec("systemctl stop docker")
localDir, err := loadLocalDir()
if err != nil {
return errors.New(stdout)
return err
}
baseDir := fmt.Sprintf("%s/system/%s", localDir, snap.Name)
if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) {
_ = os.MkdirAll(baseDir, os.ModePerm)
}
if _, err := os.Stat("/etc/systemd/system/1panel.service"); err == nil {
if err := fileOp.Compress([]string{dataDir}, backupDockerDir, "docker_data.tar.gz", files.TarGz); err != nil {
global.LOG.Errorf("snapshot backup docker data dir %s failed, err: %v", dataDir, err)
return err
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"recover_status": constant.StatusWaiting})
go func() {
operation := "recover"
if isReTry {
operation = "re-recover"
}
if !isReTry || snap.InterruptStep == "Download" || (isReTry && req.ReDownload) {
ok, err := client.Download(fmt.Sprintf("system_snapshot/%s.tar.gz", snap.Name), fmt.Sprintf("%s/%s.tar.gz", baseDir, snap.Name))
if err != nil || !ok {
if req.ReDownload {
updateRecoverStatus(snap.ID, snap.InterruptStep, constant.StatusFailed, fmt.Sprintf("download file %s from %s failed, err: %v", snap.Name, backup.Type, err))
return
}
updateRecoverStatus(snap.ID, "Download", constant.StatusFailed, fmt.Sprintf("download file %s from %s failed, err: %v", snap.Name, backup.Type, err))
return
}
isReTry = false
}
fileOp := files.NewFileOp()
if !isReTry || snap.InterruptStep == "Decompress" || (isReTry && req.ReDownload) {
if err := fileOp.Decompress(fmt.Sprintf("%s/%s.tar.gz", baseDir, snap.Name), baseDir, files.TarGz); err != nil {
if req.ReDownload {
updateRecoverStatus(snap.ID, snap.InterruptStep, constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
return
}
updateRecoverStatus(snap.ID, "Decompress", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
return
}
isReTry = false
}
rootDir := fmt.Sprintf("%s/%s", baseDir, snap.Name)
originalDir := fmt.Sprintf("%s/original/", baseDir)
snapJson, err := u.readFromJson(fmt.Sprintf("%s/snapshot.json", rootDir))
if err != nil {
updateRecoverStatus(snap.ID, "Readjson", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
return
}
if snap.InterruptStep == "Readjson" {
isReTry = false
}
snapJson.OldPanelDataDir = global.CONF.BaseDir + "/1Panel"
snapJson.OldBackupDataDir = localDir
recoverPanelDir := fmt.Sprintf("%s/%s/1panel", baseDir, snap.Name)
liveRestore := false
if !isReTry || snap.InterruptStep == "LoadDockerJson" {
snapJson.OldDockerDataDir, liveRestore, err = u.loadDockerDataDir()
if err != nil {
updateRecoverStatus(snap.ID, "LoadDockerJson", constant.StatusFailed, fmt.Sprintf("load docker data dir failed, err: %v", err))
return
}
isReTry = false
}
if liveRestore {
if err := u.updateLiveRestore(false); err != nil {
updateRecoverStatus(snap.ID, "UpdateLiveRestore", constant.StatusFailed, fmt.Sprintf("update docker daemon.json live-restore conf failed, err: %v", err))
return
}
isReTry = false
}
_ = u.saveJson(snapJson, rootDir)
_, _ = cmd.Exec("systemctl stop docker")
if !isReTry || snap.InterruptStep == "DockerDir" {
if err := u.handleDockerDatas(fileOp, operation, rootDir, snapJson.DockerDataDir); err != nil {
updateRecoverStatus(snap.ID, "DockerDir", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
if !isReTry || snap.InterruptStep == "DaemonJson" {
if err := u.handleDaemonJson(fileOp, operation, rootDir+"/docker/daemon.json", originalDir); err != nil {
updateRecoverStatus(snap.ID, "DaemonJson", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
_, _ = cmd.Exec("systemctl restart docker")
if !isReTry || snap.InterruptStep == "1PanelBinary" {
if err := u.handlePanelBinary(fileOp, operation, recoverPanelDir+"/1panel", originalDir+"/1panel"); err != nil {
updateRecoverStatus(snap.ID, "1PanelBinary", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
if !isReTry || snap.InterruptStep == "1PctlBinary" {
if err := u.handlePanelctlBinary(fileOp, operation, recoverPanelDir+"/1pctl", originalDir+"/1pctl"); err != nil {
updateRecoverStatus(snap.ID, "1PctlBinary", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
if !isReTry || snap.InterruptStep == "1PanelService" {
if err := u.handlePanelService(fileOp, operation, recoverPanelDir+"/1panel.service", originalDir+"/1panel.service"); err != nil {
updateRecoverStatus(snap.ID, "1PanelService", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
if !isReTry || snap.InterruptStep == "1PanelBackups" {
if err := u.handleBackupDatas(fileOp, operation, rootDir, snapJson.BackupDataDir); err != nil {
updateRecoverStatus(snap.ID, "1PanelBackups", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
if !isReTry || snap.InterruptStep == "1PanelData" {
if err := u.handlePanelDatas(fileOp, operation, rootDir, snapJson.PanelDataDir, "", ""); err != nil {
updateRecoverStatus(snap.ID, "1PanelData", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
fmt.Println(000)
_ = os.RemoveAll(rootDir)
fmt.Println(111)
global.LOG.Info("recover successful")
fmt.Println(222)
_, _ = cmd.Exec("systemctl daemon-reload")
fmt.Println(333)
_, _ = cmd.Exec("systemctl restart 1panel.service")
fmt.Println(444)
updateRecoverStatus(snap.ID, "", constant.StatusSuccess, "")
fmt.Println(555)
}()
return nil
}
func (u *SnapshotService) SnapshotRollback(req dto.SnapshotRecover) error {
global.LOG.Info("start to rollback now")
snap, err := snapshotRepo.Get(commonRepo.WithByID(req.ID))
if err != nil {
return err
}
if snap.InterruptStep == "Download" || snap.InterruptStep == "Decompress" || snap.InterruptStep == "Readjson" {
return nil
}
localDir, err := loadLocalDir()
if err != nil {
return err
}
fileOp := files.NewFileOp()
rootDir := fmt.Sprintf("%s/system/%s/%s", localDir, snap.Name, snap.Name)
originalDir := fmt.Sprintf("%s/system/%s/original", localDir, snap.Name)
if _, err := os.Stat(originalDir); err != nil && os.IsNotExist(err) {
return fmt.Errorf("load original dir failed, err: %s", err)
}
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"rollback_status": constant.StatusWaiting})
snapJson, err := u.readFromJson(fmt.Sprintf("%s/snapshot.json", rootDir))
if err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
return err
}
_, _ = cmd.Exec("systemctl stop docker")
if err := u.handleDockerDatas(fileOp, "rollback", originalDir, snapJson.OldDockerDataDir); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return err
}
if snap.InterruptStep == "DockerDir" {
_, _ = cmd.Exec("systemctl restart docker")
return nil
}
if err := u.handleDaemonJson(fileOp, "rollback", originalDir+"/daemon.json", ""); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return err
}
if snap.InterruptStep == "DaemonJson" {
_, _ = cmd.Exec("systemctl restart docker")
return nil
}
if _, err := os.Stat(constant.DaemonJsonPath); err == nil {
if err := fileOp.CopyFile(constant.DaemonJsonPath, backupDockerDir); err != nil {
global.LOG.Errorf("snapshot backup daemon.json failed, err: %v", err)
if snapJson.LiveRestoreEnabled {
if err := u.updateLiveRestore(true); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return err
}
}
if snap.InterruptStep == "UpdateLiveRestore" {
_, _ = cmd.Exec("systemctl daemon-reload")
_, _ = cmd.Exec("systemctl restart 1panel.service")
return nil
}
if _, err := os.Stat("/Users/slooop/go/bin/swag"); err == nil {
if err := fileOp.CopyFile("/Users/slooop/go/bin/swag", backupPanelDir); err != nil {
global.LOG.Errorf("snapshot backup 1panel failed, err: %v", err)
return err
if err := u.handlePanelBinary(fileOp, "rollback", originalDir+"/1panel", ""); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return err
}
if snap.InterruptStep == "1PanelBinary" {
_, _ = cmd.Exec("systemctl daemon-reload")
_, _ = cmd.Exec("systemctl restart 1panel.service")
return nil
}
if err := u.handlePanelctlBinary(fileOp, "rollback", originalDir+"/1pctl", ""); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return err
}
if snap.InterruptStep == "1PctlBinary" {
_, _ = cmd.Exec("systemctl daemon-reload")
_, _ = cmd.Exec("systemctl restart 1panel.service")
return nil
}
if err := u.handlePanelService(fileOp, "rollback", originalDir+"/1panel.service", ""); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return err
}
if snap.InterruptStep == "1PanelService" {
_, _ = cmd.Exec("systemctl daemon-reload")
_, _ = cmd.Exec("systemctl restart 1panel.service")
return nil
}
if err := u.handleBackupDatas(fileOp, "rollback", originalDir, snapJson.OldBackupDataDir); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return err
}
if snap.InterruptStep == "1PanelBackups" {
_, _ = cmd.Exec("systemctl daemon-reload")
_, _ = cmd.Exec("systemctl restart 1panel.service")
return nil
}
if err := u.handlePanelDatas(fileOp, "rollback", originalDir, snapJson.OldPanelDataDir, "", ""); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return err
}
if snap.InterruptStep == "1PanelData" {
_, _ = cmd.Exec("systemctl daemon-reload")
_, _ = cmd.Exec("systemctl restart 1panel.service")
return nil
}
fmt.Println(000)
_ = os.RemoveAll(rootDir)
fmt.Println(111)
global.LOG.Info("rollback successful")
fmt.Println(222)
_, _ = cmd.Exec("systemctl daemon-reload")
fmt.Println(333)
_, _ = cmd.Exec("systemctl restart 1panel.service")
fmt.Println(444)
updateRollbackStatus(snap.ID, constant.StatusSuccess, "")
fmt.Println(555)
return nil
}
func (u *SnapshotService) saveJson(snapJson SnapshotJson, path string) error {
remarkInfo, _ := json.MarshalIndent(snapJson, "", "\t")
if err := ioutil.WriteFile(fmt.Sprintf("%s/snapshot.json", path), remarkInfo, 0640); err != nil {
return err
}
return nil
}
func (u *SnapshotService) readFromJson(path string) (SnapshotJson, error) {
var snap SnapshotJson
if _, err := os.Stat(path); err != nil {
return snap, fmt.Errorf("find snapshot json file in recover package failed, err: %v", err)
}
fileByte, err := os.ReadFile(path)
if err != nil {
return snap, fmt.Errorf("read file from path %s failed, err: %v", path, err)
}
if err := json.Unmarshal(fileByte, &snap); err != nil {
return snap, fmt.Errorf("unmarshal snapjson failed, err: %v", err)
}
return snap, nil
}
func (u *SnapshotService) handleDockerDatas(fileOp files.FileOp, operation string, source, target string) error {
switch operation {
case "snapshot":
if err := u.handleTar(source, target, "docker_data.tar.gz", ""); err != nil {
return fmt.Errorf("backup docker data failed, err: %v", err)
}
case "recover":
if err := u.handleTar(target, fmt.Sprintf("%s/original", filepath.Join(source, "../")), "docker_data.tar.gz", ""); err != nil {
return fmt.Errorf("backup docker data failed, err: %v", err)
}
if err := u.handleUnTar(source+"/docker/docker_data.tar.gz", target); err != nil {
return fmt.Errorf("recover docker data failed, err: %v", err)
}
case "re-recover":
if err := u.handleUnTar(source+"/docker/docker_data.tar.gz", target); err != nil {
return fmt.Errorf("re-recover docker data failed, err: %v", err)
}
case "rollback":
if err := u.handleUnTar(source+"/docker_data.tar.gz", target); err != nil {
return fmt.Errorf("rollback docker data failed, err: %v", err)
}
}
if _, err := os.Stat("/etc/systemd/system/1panel.service"); err == nil {
if err := fileOp.CopyFile("/etc/systemd/system/1panel.service", backupPanelDir); err != nil {
global.LOG.Errorf("snapshot backup 1panel.service failed, err: %v", err)
return err
global.LOG.Info("handle docker data dir successful!")
return nil
}
func (u *SnapshotService) handleDaemonJson(fileOp files.FileOp, operation string, source, target string) error {
daemonJsonPath := "/etc/docker/daemon.json"
if operation == "snapshot" || operation == "recover" {
_, err := os.Stat(daemonJsonPath)
if os.IsNotExist(err) {
global.LOG.Info("no daemon.josn in snapshot and system now, nothing happened")
}
if err == nil {
if err := fileOp.CopyFile(daemonJsonPath, target); err != nil {
return fmt.Errorf("backup docker daemon.json failed, err: %v", err)
}
}
}
if _, err := os.Stat("/usr/local/bin/1panelctl"); err == nil {
if err := fileOp.CopyFile("/usr/local/bin/1panelctl", backupPanelDir); err != nil {
global.LOG.Errorf("snapshot backup 1panelctl failed, err: %v", err)
return err
if operation == "recover" || operation == "rollback" || operation == "re-recover" {
_, sourceErr := os.Stat(source)
if os.IsNotExist(sourceErr) {
_ = os.Remove(daemonJsonPath)
}
if sourceErr == nil {
if err := fileOp.CopyFile(source, "/etc/docker"); err != nil {
return fmt.Errorf("recover docker daemon.json failed, err: %v", err)
}
}
}
if _, err := os.Stat(global.CONF.System.DataDir); err == nil {
if err := fileOp.Compress([]string{global.CONF.System.DataDir}, backupPanelDir, "1panel_data.tar.gz", files.TarGz); err != nil {
global.LOG.Errorf("snapshot backup 1panel data %s failed, err: %v", global.CONF.System.DataDir, err)
return err
global.LOG.Info("handle docker daemon.json successful!")
return nil
}
func (u *SnapshotService) handlePanelBinary(fileOp files.FileOp, operation string, source, target string) error {
panelPath := "/usr/local/bin/1panel"
if operation == "snapshot" || operation == "recover" {
if _, err := os.Stat(panelPath); err != nil {
return fmt.Errorf("1panel binary is not found in %s, err: %v", panelPath, err)
} else {
if err := cpBinary(panelPath, target); err != nil {
return fmt.Errorf("backup 1panel binary failed, err: %v", err)
}
}
}
if operation == "recover" || operation == "rollback" || operation == "re-recover" {
if _, err := os.Stat(source); err != nil {
return fmt.Errorf("1panel binary is not found in snapshot, err: %v", err)
} else {
if err := cpBinary(source, "/usr/local/bin/1panel"); err != nil {
return fmt.Errorf("recover 1panel binary failed, err: %v", err)
}
}
}
global.LOG.Info("handle binary panel successful!")
return nil
}
func (u *SnapshotService) handlePanelctlBinary(fileOp files.FileOp, operation string, source, target string) error {
panelctlPath := "/usr/local/bin/1pctl"
if operation == "snapshot" || operation == "recover" {
if _, err := os.Stat(panelctlPath); err != nil {
return fmt.Errorf("1pctl binary is not found in %s, err: %v", panelctlPath, err)
} else {
if err := cpBinary(panelctlPath, target); err != nil {
return fmt.Errorf("backup 1pctl binary failed, err: %v", err)
}
}
}
if operation == "recover" || operation == "rollback" || operation == "re-recover" {
if _, err := os.Stat(source); err != nil {
return fmt.Errorf("1pctl binary is not found in snapshot, err: %v", err)
} else {
if err := cpBinary(source, "/usr/local/bin/1pctl"); err != nil {
return fmt.Errorf("recover 1pctl binary failed, err: %v", err)
}
}
}
global.LOG.Info("handle binary 1pactl successful!")
return nil
}
func (u *SnapshotService) handlePanelService(fileOp files.FileOp, operation string, source, target string) error {
panelServicePath := "/etc/systemd/system/1panel.service"
if operation == "snapshot" || operation == "recover" {
if _, err := os.Stat(panelServicePath); err != nil {
return fmt.Errorf("1panel service is not found in %s, err: %v", panelServicePath, err)
} else {
if err := cpBinary(panelServicePath, target); err != nil {
return fmt.Errorf("backup 1panel service failed, err: %v", err)
}
}
}
if operation == "recover" || operation == "rollback" || operation == "re-recover" {
if _, err := os.Stat(source); err != nil {
return fmt.Errorf("1panel service is not found in snapshot, err: %v", err)
} else {
if err := cpBinary(source, "/etc/systemd/system/1panel.service"); err != nil {
return fmt.Errorf("recover 1panel service failed, err: %v", err)
}
}
}
global.LOG.Info("handle panel service successful!")
return nil
}
func (u *SnapshotService) handleBackupDatas(fileOp files.FileOp, operation string, source, target string) error {
switch operation {
case "snapshot":
if err := u.handleTar(source, target, "1panel_backup.tar.gz", "./system"); err != nil {
return fmt.Errorf("backup panel local backup dir data failed, err: %v", err)
}
case "recover":
if err := u.handleTar(target, fmt.Sprintf("%s/original", filepath.Join(source, "../")), "1panel_backup.tar.gz", "./system"); err != nil {
return fmt.Errorf("restore original local backup dir data failed, err: %v", err)
}
if err := u.handleUnTar(source+"/1panel/1panel_backup.tar.gz", target); err != nil {
return fmt.Errorf("recover local backup dir data failed, err: %v", err)
}
case "re-recover":
if err := u.handleUnTar(source+"/1panel/1panel_backup.tar.gz", target); err != nil {
return fmt.Errorf("retry recover local backup dir data failed, err: %v", err)
}
case "rollback":
if err := u.handleUnTar(source+"/1panel_backup.tar.gz", target); err != nil {
return fmt.Errorf("rollback local backup dir data failed, err: %v", err)
}
}
global.LOG.Info("handle backup data successful!")
return nil
}
func (u *SnapshotService) handlePanelDatas(fileOp files.FileOp, operation string, source, target, backupDir, dockerDir string) error {
switch operation {
case "snapshot":
exclusionRules := ""
if strings.Contains(backupDir, source) {
exclusionRules += ("." + strings.ReplaceAll(backupDir, source, "") + ";")
}
if strings.Contains(dockerDir, source) {
exclusionRules += ("." + strings.ReplaceAll(dockerDir, source, "") + ";")
}
if err := u.handleTar(source, target, "1panel_data.tar.gz", exclusionRules); err != nil {
return fmt.Errorf("backup panel data failed, err: %v", err)
}
case "recover":
exclusionRules := ""
if strings.Contains(backupDir, target) {
exclusionRules += ("1Panel" + strings.ReplaceAll(backupDir, target, "") + ";")
}
if strings.Contains(dockerDir, target) {
exclusionRules += ("1Panel" + strings.ReplaceAll(dockerDir, target, "") + ";")
}
if err := u.handleTar(target, fmt.Sprintf("%s/original", filepath.Join(source, "../")), "1panel_data.tar.gz", exclusionRules); err != nil {
return fmt.Errorf("restore original panel data failed, err: %v", err)
}
if err := u.handleUnTar(source+"/1panel/1panel_data.tar.gz", target); err != nil {
return fmt.Errorf("recover panel data failed, err: %v", err)
}
case "re-recover":
if err := u.handleUnTar(source+"/1panel/1panel_data.tar.gz", target); err != nil {
return fmt.Errorf("retry recover panel data failed, err: %v", err)
}
case "rollback":
if err := u.handleUnTar(source+"/1panel_data.tar.gz", target); err != nil {
return fmt.Errorf("rollback panel data failed, err: %v", err)
}
}
global.LOG.Info("handle panel data successful!")
return nil
}
func (u *SnapshotService) loadDockerDataDir() (string, bool, error) {
client, err := docker.NewDockerClient()
if err != nil {
return "", false, fmt.Errorf("new docker client failed, err: %v", err)
}
info, err := client.Info(context.Background())
if err != nil {
return "", false, fmt.Errorf("load docker info failed, err: %v", err)
}
return info.DockerRootDir, info.LiveRestoreEnabled, nil
}
func (u *SnapshotService) Delete(req dto.BatchDeleteReq) error {
backups, _ := snapshotRepo.GetList(commonRepo.WithIdsIn(req.Ids))
localDir, err := loadLocalDir()
if err != nil {
return err
}
for _, snap := range backups {
if _, err := os.Stat(fmt.Sprintf("%s/system/%s/%s.tar.gz", localDir, snap.Name, snap.Name)); err == nil {
_ = os.Remove(fmt.Sprintf("%s/system/%s/%s.tar.gz", localDir, snap.Name, snap.Name))
}
}
if err := fileOp.Compress([]string{rootDir}, fmt.Sprintf("%s/system", localDir), fmt.Sprintf("1panel_backup_%s.tar.gz", timeNow), files.TarGz); err != nil {
if err := snapshotRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil {
return err
}
snap := model.Snapshot{
Name: "1panel_backup_" + timeNow,
Description: req.Description,
BackupType: req.BackupType,
Status: constant.StatusWaiting,
return nil
}
func updateSnapshotStatus(id uint, status string, message string) {
if status != constant.StatusSuccess {
global.LOG.Errorf("snapshot failed, err: %s", message)
}
_ = snapshotRepo.Create(&snap)
go func() {
localPath := fmt.Sprintf("%s/system/1panel_backup_%s.tar.gz", localDir, timeNow)
if ok, err := backupAccont.Upload(localPath, fmt.Sprintf("system_snapshot/1panel_backup_%s.tar.gz", timeNow)); err != nil || !ok {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
global.LOG.Errorf("upload snapshot to %s failed, err: %v", backup.Type, err)
return
if err := snapshotRepo.Update(id, map[string]interface{}{
"status": status,
"message": message,
}); err != nil {
global.LOG.Errorf("update snap snapshot status failed, err: %v", err)
}
}
func updateRecoverStatus(id uint, interruptStep, status string, message string) {
if status != constant.StatusSuccess {
global.LOG.Errorf("recover failed, err: %s", message)
}
if err := snapshotRepo.Update(id, map[string]interface{}{
"interrupt_step": interruptStep,
"recover_status": status,
"recover_message": message,
"last_recovered_at": time.Now().Format("2006-01-02 15:04:05"),
}); err != nil {
global.LOG.Errorf("update snap recover status failed, err: %v", err)
}
}
func updateRollbackStatus(id uint, status string, message string) {
if status == constant.StatusSuccess {
if err := snapshotRepo.Update(id, map[string]interface{}{
"recover_status": "",
"recover_message": "",
"interrupt_step": "",
"rollback_status": "",
"rollback_message": "",
"last_rollbacked_at": time.Now().Format("2006-01-02 15:04:05"),
}); err != nil {
global.LOG.Errorf("update snap recover status failed, err: %v", err)
}
snap.Status = constant.StatusSuccess
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusSuccess})
global.LOG.Infof("upload snapshot to %s success", backup.Type)
}()
return
}
global.LOG.Errorf("rollback failed, err: %s", message)
if err := snapshotRepo.Update(id, map[string]interface{}{
"rollback_status": status,
"rollback_message": message,
"last_rollbacked_at": time.Now().Format("2006-01-02 15:04:05"),
}); err != nil {
global.LOG.Errorf("update snap recover status failed, err: %v", err)
}
}
func cpBinary(src, dst string) error {
stderr, err := cmd.Exec(fmt.Sprintf("\\cp -f %s %s", src, dst))
if err != nil {
return errors.New(stderr)
}
return nil
}
func (u *SnapshotService) updateLiveRestore(enabled bool) error {
if _, err := os.Stat(constant.DaemonJsonPath); err != nil {
return fmt.Errorf("load docker daemon.json conf failed, err: %v", err)
}
file, err := ioutil.ReadFile(constant.DaemonJsonPath)
if err != nil {
return err
}
deamonMap := make(map[string]interface{})
_ = json.Unmarshal(file, &deamonMap)
if !enabled {
delete(deamonMap, "live-restore")
} else {
deamonMap["live-restore"] = enabled
}
newJson, err := json.MarshalIndent(deamonMap, "", "\t")
if err != nil {
return err
}
if err := ioutil.WriteFile(constant.DaemonJsonPath, newJson, 0640); err != nil {
return err
}
stdout, err := cmd.Exec("systemctl restart docker")
if err != nil {
return errors.New(stdout)
}
time.Sleep(10 * time.Second)
return nil
}
func (u *SnapshotService) handleTar(sourceDir, targetDir, name, exclusionRules string) error {
if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(targetDir, os.ModePerm); err != nil {
return err
}
}
exStr := []string{"--warning=no-file-changed"}
exStr = append(exStr, "-zcf")
exStr = append(exStr, targetDir+"/"+name)
excludes := strings.Split(exclusionRules, ";")
for _, exclude := range excludes {
if len(exclude) == 0 {
continue
}
exStr = append(exStr, "--exclude")
exStr = append(exStr, exclude)
}
exStr = append(exStr, "-C")
exStr = append(exStr, sourceDir)
exStr = append(exStr, ".")
cmd := exec.Command("tar", exStr...)
stdout, err := cmd.CombinedOutput()
if err != nil {
return errors.New(string(stdout))
}
return nil
}
func (u *SnapshotService) handleUnTar(sourceDir, targetDir string) error {
if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(targetDir, os.ModePerm); err != nil {
return err
}
}
exStr := []string{}
exStr = append(exStr, "zxf")
exStr = append(exStr, sourceDir)
exStr = append(exStr, "-C")
exStr = append(exStr, targetDir)
exStr = append(exStr, ".")
cmd := exec.Command("tar", exStr...)
stdout, err := cmd.CombinedOutput()
if err != nil {
return errors.New(string(stdout))
}
return nil
}

56
backend/app/service/snapshot_test.go

@ -1,34 +1,60 @@
package service
import (
"context"
"fmt"
"io/ioutil"
"strings"
"testing"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/init/db"
"github.com/1Panel-dev/1Panel/backend/init/viper"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/google/go-github/github"
)
func TestSnaa(t *testing.T) {
fileOp := files.NewFileOp()
fmt.Println(fileOp.CopyFile("/Users/slooop/Documents/编码规范.pdf", "/Users/slooop/Downloads"))
// fmt.Println(fileOp.Compress([]string{"/Users/slooop/Documents/编码规范.pdf", "/Users/slooop/Downloads/1Panel.db"}, "/Users/slooop/Downloads/", "test.tar.gz", files.TarGz))
}
func TestOss(t *testing.T) {
func TestDw(t *testing.T) {
viper.Init()
db.Init()
var backup model.BackupAccount
if err := global.DB.Where("id = ?", 6).First(&backup).Error; err != nil {
backup, err := backupRepo.Get(commonRepo.WithByType("OSS"))
if err != nil {
fmt.Println(err)
}
backupAccont, err := NewIBackupService().NewClient(&backup)
client, err := NewIBackupService().NewClient(&backup)
if err != nil {
fmt.Println(err)
}
fmt.Println(backupAccont.Upload("/Users/slooop/Downloads/1Panel.db", "database/1Panel.db"))
fmt.Println(client.Download("system_snapshot/1panel_snapshot_20230112135640.tar.gz", "/opt/1Panel/data/backup/system/test.tar.gz"))
}
func TestDi(t *testing.T) {
docker := "var/lib/docker"
fmt.Println(docker[strings.LastIndex(docker, "/"):])
fmt.Println(docker[:strings.LastIndex(docker, "/")])
}
func TestGit(t *testing.T) {
client := github.NewClient(nil)
stats, _, err := client.Repositories.GetLatestRelease(context.Background(), "KubeOperator", "KubeOperator")
fmt.Println(github.Timestamp(*stats.PublishedAt), err)
}
func TestSdasd(t *testing.T) {
u := NewISnapshotService()
var snapjson SnapshotJson
snapjson, _ = u.readFromJson("/Users/slooop/Downloads/snapshot.json")
fmt.Println(111, snapjson)
// if err := ioutil.WriteFile("/Users/slooop/Downloads/snapshot.json", []byte("111xxxxx"), 0640); err != nil {
// fmt.Println(err)
// }
}
func TestCp(t *testing.T) {
_, err := ioutil.ReadFile("/Users/slooop/Downloads/test/main")
if err != nil {
fmt.Println(err)
}
if err := ioutil.WriteFile("/Users/slooop/Downloads/test/main", []byte("sdadasd"), 0640); err != nil {
fmt.Println(err)
}
}

5
backend/configs/cache.go

@ -1,5 +0,0 @@
package configs
type Cache struct {
Path string `mapstructure:"path"`
}

5
backend/configs/config.go

@ -1,11 +1,10 @@
package configs
type ServerConfig struct {
Sqlite Sqlite `mapstructure:"sqlite"`
BaseDir string `mapstructure:"base_dir"`
System System `mapstructure:"system"`
Sqlite Sqlite `mapstructure:"sqlite"`
LogConfig LogConfig `mapstructure:"log"`
CORS CORS `mapstructure:"cors"`
Encrypt Encrypt `mapstructure:"encrypt"`
Csrf Csrf `mapstructure:"csrf"`
Cache Cache `mapstructure:"cache"`
}

5
backend/configs/csrf.go

@ -1,5 +0,0 @@
package configs
type Csrf struct {
Key string `mapstructure:"key" json:"key" yaml:"key"`
}

3
backend/configs/system.go

@ -3,7 +3,8 @@ package configs
type System struct {
Port int `mapstructure:"port"`
DbType string `mapstructure:"db_type"`
Level string `mapstructure:"level"`
DataDir string `mapstructure:"data_dir"`
Cache string `mapstructure:"cache"`
Backup string `mapstructure:"backup"`
AppOss string `mapstructure:"app_oss"`
}

2
backend/constant/container.go

@ -16,5 +16,5 @@ const (
TmpDockerBuildDir = "/opt/1Panel/data/docker/build"
TmpComposeBuildDir = "/opt/1Panel/data/docker/compose"
DaemonJsonPath = "/tmp/docker/daemon.json"
DaemonJsonPath = "/etc/docker/daemon.json"
)

9
backend/init/cache/cache.go vendored

@ -1,18 +1,19 @@
package cache
import (
"time"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/init/cache/badger_db"
"github.com/dgraph-io/badger/v3"
"time"
)
func Init() {
c := global.CONF.Cache
c := global.CONF.System.Cache
options := badger.Options{
Dir: c.Path,
ValueDir: c.Path,
Dir: c,
ValueDir: c,
ValueLogFileSize: 102400000,
ValueLogMaxEntries: 100000,
VLogPercentile: 0.1,

2
backend/init/migration/migrate.go

@ -19,7 +19,7 @@ func Init() {
migrations.AddTableImageRepo,
migrations.AddTableWebsite,
migrations.AddTableDatabaseMysql,
migrations.AddTableSnapshot,
migrations.AddTableSnap,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

7
backend/init/migration/migrations/init.go

@ -126,6 +126,9 @@ var AddTableSetting = &gormigrate.Migration{
if err := tx.Create(&model.Setting{Key: "DingVars", Value: ""}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "SystemVersion", Value: "v1.0.0"}).Error; err != nil {
return err
}
return nil
},
}
@ -203,8 +206,8 @@ var AddTableWebsite = &gormigrate.Migration{
},
}
var AddTableSnapshot = &gormigrate.Migration{
ID: "20230106-add-table-snapshot",
var AddTableSnap = &gormigrate.Migration{
ID: "20230106-add-table-snap",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Snapshot{}); err != nil {
return err

29
backend/middleware/csrf.go

@ -1,29 +0,0 @@
package middleware
import (
"net/http"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/gin-gonic/gin"
"github.com/gorilla/csrf"
adapter "github.com/gwatts/gin-adapter"
)
func CSRF() gin.HandlerFunc {
csrfMd := csrf.Protect(
[]byte(global.CONF.Csrf.Key),
csrf.Path("/api"),
csrf.ErrorHandler(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte("csrf token invalid"))
})),
)
return adapter.Wrap(csrfMd)
}
func LoadCsrfToken() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("X-CSRF-TOKEN", csrf.Token(c.Request))
}
}

11
backend/router/ro_setting.go

@ -9,16 +9,15 @@ import (
type SettingRouter struct{}
func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) {
baseRouter := Router.Group("settings")
settingRouter := Router.Group("settings").
Use(middleware.JwtAuth()).
Use(middleware.SessionAuth()).
Use(middleware.PasswordExpired())
baseApi := v1.ApiGroupApp.BaseApi
{
baseRouter.POST("/search", baseApi.GetSettingInfo)
baseRouter.POST("/expired/handle", baseApi.HandlePasswordExpired)
baseRouter.POST("/update", baseApi.UpdateSetting)
settingRouter.POST("/search", baseApi.GetSettingInfo)
settingRouter.POST("/expired/handle", baseApi.HandlePasswordExpired)
settingRouter.POST("/update", baseApi.UpdateSetting)
settingRouter.POST("/password/update", baseApi.UpdatePassword)
settingRouter.POST("/time/sync", baseApi.SyncTime)
settingRouter.POST("/monitor/clean", baseApi.CleanMonitor)
@ -26,5 +25,9 @@ func (s *SettingRouter) InitSettingRouter(Router *gin.RouterGroup) {
settingRouter.POST("/mfa/bind", baseApi.MFABind)
settingRouter.POST("/snapshot", baseApi.CreateSnapshot)
settingRouter.POST("/snapshot/search", baseApi.SearchSnapshot)
settingRouter.POST("/snapshot/del", baseApi.DeleteSnapshot)
settingRouter.POST("/snapshot/recover", baseApi.RecoverSnapshot)
settingRouter.POST("/snapshot/rollback", baseApi.RollbackSnapshot)
settingRouter.GET("/upgrade", baseApi.GetUpgradeInfo)
}
}

5
backend/server/server.go

@ -3,9 +3,10 @@ package server
import (
"encoding/gob"
"fmt"
"time"
"github.com/1Panel-dev/1Panel/backend/init/app"
"github.com/1Panel-dev/1Panel/backend/init/business"
"time"
"github.com/1Panel-dev/1Panel/backend/cron"
"github.com/1Panel-dev/1Panel/backend/init/cache"
@ -34,7 +35,7 @@ func Start() {
gob.Register(psession.SessionUser{})
cache.Init()
session.Init()
gin.SetMode(global.CONF.System.Level)
gin.SetMode("debug")
cron.Run()
business.Init()

339
cmd/server/docs/docs.go

@ -5976,28 +5976,6 @@ var doc = `{
}
}
},
"/settings/daemonjson": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "加载 docker 配置路径",
"tags": [
"System Setting"
],
"summary": "Load daemon.json path",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/settings/expired/handle": {
"post": {
"security": [
@ -6188,6 +6166,238 @@ var doc = `{
}
}
},
"/settings/snapshot": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "创建系统快照",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Create system backup",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SnapshotCreate"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"name",
"description"
],
"formatEN": "Create system backup [name][description]",
"formatZH": "创建系统快照 [name][description]",
"paramKeys": []
}
}
},
"/settings/snapshot/del": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "删除系统快照",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Delete system backup",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.BatchDeleteReq"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [
{
"db": "snapshots",
"input_colume": "id",
"input_value": "ids",
"isList": true,
"output_colume": "name",
"output_value": "name"
}
],
"bodyKeys": [
"ids"
],
"formatEN": "Delete system backup [name]",
"formatZH": "删除系统快照 [name]",
"paramKeys": []
}
}
},
"/settings/snapshot/recover": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "从系统快照恢复",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Recover system backup",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SnapshotRecover"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [
{
"db": "snapshots",
"input_colume": "id",
"input_value": "id",
"isList": false,
"output_colume": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "Recover from system backup [name]",
"formatZH": "从系统快照 [name] 恢复",
"paramKeys": []
}
}
},
"/settings/snapshot/rollback": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "从系统快照回滚",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Rollback system backup",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SnapshotRecover"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [
{
"db": "snapshots",
"input_colume": "id",
"input_value": "id",
"isList": false,
"output_colume": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "Rollback from system backup [name]",
"formatZH": "从系统快照 [name] 回滚",
"paramKeys": []
}
}
},
"/settings/snapshot/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取系统快照列表分页",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Page system snapshot",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.PageInfo"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/settings/time/sync": {
"post": {
"security": [
@ -6260,6 +6470,28 @@ var doc = `{
}
}
},
"/settings/upgrade": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "加载系统更新信息",
"tags": [
"System Setting"
],
"summary": "Load upgrade info",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UpgradeInfo"
}
}
}
}
},
"/websites": {
"post": {
"security": [
@ -8629,6 +8861,9 @@ var doc = `{
},
"status": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
@ -9960,6 +10195,9 @@ var doc = `{
"sessionTimeout": {
"type": "string"
},
"systemVersion": {
"type": "string"
},
"theme": {
"type": "string"
},
@ -9985,6 +10223,63 @@ var doc = `{
}
}
},
"dto.SnapshotCreate": {
"type": "object",
"required": [
"from"
],
"properties": {
"description": {
"type": "string"
},
"from": {
"type": "string",
"enum": [
"OSS",
"S3",
"SFTP",
"MINIO"
]
}
}
},
"dto.SnapshotRecover": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "integer"
},
"isNew": {
"type": "boolean"
},
"reDownload": {
"type": "boolean"
}
}
},
"dto.UpgradeInfo": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"newVersion": {
"type": "string"
},
"publishedAt": {
"type": "string"
},
"releaseNote": {
"type": "string"
},
"tag": {
"type": "string"
}
}
},
"dto.UploadRecover": {
"type": "object",
"required": [

339
cmd/server/docs/swagger.json

@ -5962,28 +5962,6 @@
}
}
},
"/settings/daemonjson": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "加载 docker 配置路径",
"tags": [
"System Setting"
],
"summary": "Load daemon.json path",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/settings/expired/handle": {
"post": {
"security": [
@ -6174,6 +6152,238 @@
}
}
},
"/settings/snapshot": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "创建系统快照",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Create system backup",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SnapshotCreate"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [],
"bodyKeys": [
"name",
"description"
],
"formatEN": "Create system backup [name][description]",
"formatZH": "创建系统快照 [name][description]",
"paramKeys": []
}
}
},
"/settings/snapshot/del": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "删除系统快照",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Delete system backup",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.BatchDeleteReq"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [
{
"db": "snapshots",
"input_colume": "id",
"input_value": "ids",
"isList": true,
"output_colume": "name",
"output_value": "name"
}
],
"bodyKeys": [
"ids"
],
"formatEN": "Delete system backup [name]",
"formatZH": "删除系统快照 [name]",
"paramKeys": []
}
}
},
"/settings/snapshot/recover": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "从系统快照恢复",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Recover system backup",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SnapshotRecover"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [
{
"db": "snapshots",
"input_colume": "id",
"input_value": "id",
"isList": false,
"output_colume": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "Recover from system backup [name]",
"formatZH": "从系统快照 [name] 恢复",
"paramKeys": []
}
}
},
"/settings/snapshot/rollback": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "从系统快照回滚",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Rollback system backup",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SnapshotRecover"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"x-panel-log": {
"BeforeFuntions": [
{
"db": "snapshots",
"input_colume": "id",
"input_value": "id",
"isList": false,
"output_colume": "name",
"output_value": "name"
}
],
"bodyKeys": [
"id"
],
"formatEN": "Rollback from system backup [name]",
"formatZH": "从系统快照 [name] 回滚",
"paramKeys": []
}
}
},
"/settings/snapshot/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取系统快照列表分页",
"consumes": [
"application/json"
],
"tags": [
"System Setting"
],
"summary": "Page system snapshot",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.PageInfo"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/settings/time/sync": {
"post": {
"security": [
@ -6246,6 +6456,28 @@
}
}
},
"/settings/upgrade": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "加载系统更新信息",
"tags": [
"System Setting"
],
"summary": "Load upgrade info",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.UpgradeInfo"
}
}
}
}
},
"/websites": {
"post": {
"security": [
@ -8615,6 +8847,9 @@
},
"status": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
@ -9946,6 +10181,9 @@
"sessionTimeout": {
"type": "string"
},
"systemVersion": {
"type": "string"
},
"theme": {
"type": "string"
},
@ -9971,6 +10209,63 @@
}
}
},
"dto.SnapshotCreate": {
"type": "object",
"required": [
"from"
],
"properties": {
"description": {
"type": "string"
},
"from": {
"type": "string",
"enum": [
"OSS",
"S3",
"SFTP",
"MINIO"
]
}
}
},
"dto.SnapshotRecover": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "integer"
},
"isNew": {
"type": "boolean"
},
"reDownload": {
"type": "boolean"
}
}
},
"dto.UpgradeInfo": {
"type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"newVersion": {
"type": "string"
},
"publishedAt": {
"type": "string"
},
"releaseNote": {
"type": "string"
},
"tag": {
"type": "string"
}
}
},
"dto.UploadRecover": {
"type": "object",
"required": [

217
cmd/server/docs/swagger.yaml

@ -391,6 +391,8 @@ definitions:
type: array
status:
type: string
version:
type: string
type: object
dto.DaemonJsonUpdateByFile:
properties:
@ -1278,6 +1280,8 @@ definitions:
type: string
sessionTimeout:
type: string
systemVersion:
type: string
theme:
type: string
userName:
@ -1294,6 +1298,44 @@ definitions:
required:
- key
type: object
dto.SnapshotCreate:
properties:
description:
type: string
from:
enum:
- OSS
- S3
- SFTP
- MINIO
type: string
required:
- from
type: object
dto.SnapshotRecover:
properties:
id:
type: integer
isNew:
type: boolean
reDownload:
type: boolean
required:
- id
type: object
dto.UpgradeInfo:
properties:
createdAt:
type: string
newVersion:
type: string
publishedAt:
type: string
releaseNote:
type: string
tag:
type: string
type: object
dto.UploadRecover:
properties:
dbName:
@ -6314,19 +6356,6 @@ paths:
formatEN: Update nginx conf [domain]
formatZH: 更新 nginx 配置 [domain]
paramKeys: []
/settings/daemonjson:
get:
description: 加载 docker 配置路径
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
summary: Load daemon.json path
tags:
- System Setting
/settings/expired/handle:
post:
consumes:
@ -6448,6 +6477,155 @@ paths:
summary: Load system setting info
tags:
- System Setting
/settings/snapshot:
post:
consumes:
- application/json
description: 创建系统快照
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SnapshotCreate'
responses:
"200":
description: ""
security:
- ApiKeyAuth: []
summary: Create system backup
tags:
- System Setting
x-panel-log:
BeforeFuntions: []
bodyKeys:
- name
- description
formatEN: Create system backup [name][description]
formatZH: 创建系统快照 [name][description]
paramKeys: []
/settings/snapshot/del:
post:
consumes:
- application/json
description: 删除系统快照
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.BatchDeleteReq'
responses:
"200":
description: ""
security:
- ApiKeyAuth: []
summary: Delete system backup
tags:
- System Setting
x-panel-log:
BeforeFuntions:
- db: snapshots
input_colume: id
input_value: ids
isList: true
output_colume: name
output_value: name
bodyKeys:
- ids
formatEN: Delete system backup [name]
formatZH: 删除系统快照 [name]
paramKeys: []
/settings/snapshot/recover:
post:
consumes:
- application/json
description: 从系统快照恢复
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SnapshotRecover'
responses:
"200":
description: ""
security:
- ApiKeyAuth: []
summary: Recover system backup
tags:
- System Setting
x-panel-log:
BeforeFuntions:
- db: snapshots
input_colume: id
input_value: id
isList: false
output_colume: name
output_value: name
bodyKeys:
- id
formatEN: Recover from system backup [name]
formatZH: 从系统快照 [name] 恢复
paramKeys: []
/settings/snapshot/rollback:
post:
consumes:
- application/json
description: 从系统快照回滚
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SnapshotRecover'
responses:
"200":
description: ""
security:
- ApiKeyAuth: []
summary: Rollback system backup
tags:
- System Setting
x-panel-log:
BeforeFuntions:
- db: snapshots
input_colume: id
input_value: id
isList: false
output_colume: name
output_value: name
bodyKeys:
- id
formatEN: Rollback from system backup [name]
formatZH: 从系统快照 [name] 回滚
paramKeys: []
/settings/snapshot/search:
post:
consumes:
- application/json
description: 获取系统快照列表分页
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.PageInfo'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.PageResult'
security:
- ApiKeyAuth: []
summary: Page system snapshot
tags:
- System Setting
/settings/time/sync:
post:
description: 系统时间同步
@ -6495,6 +6673,19 @@ paths:
formatEN: update system setting [key] => [value]
formatZH: 修改系统配置 [key] => [value]
paramKeys: []
/settings/upgrade:
get:
description: 加载系统更新信息
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.UpgradeInfo'
security:
- ApiKeyAuth: []
summary: Load upgrade info
tags:
- System Setting
/websites:
post:
consumes:

14
frontend/package-lock.json generated

@ -22,6 +22,7 @@
"fit2cloud-ui-plus": "^0.0.1-beta.15",
"js-base64": "^3.7.2",
"js-md5": "^0.7.3",
"md-editor-v3": "^2.7.2",
"monaco-editor": "^0.34.0",
"nprogress": "^0.2.0",
"pinia": "^2.0.12",
@ -9027,6 +9028,14 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/md-editor-v3": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/md-editor-v3/-/md-editor-v3-2.7.2.tgz",
"integrity": "sha512-CyLG7yZhMyKplXO/MYIccpL0AOcnys74cMpbBG77rmXWlANAmzLrznUU++g6MohTv3DCRNTz+5Uh/w9h9P2sSA==",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
@ -20493,6 +20502,11 @@
"integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==",
"dev": true
},
"md-editor-v3": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/md-editor-v3/-/md-editor-v3-2.7.2.tgz",
"integrity": "sha512-CyLG7yZhMyKplXO/MYIccpL0AOcnys74cMpbBG77rmXWlANAmzLrznUU++g6MohTv3DCRNTz+5Uh/w9h9P2sSA=="
},
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",

1
frontend/package.json

@ -34,6 +34,7 @@
"fit2cloud-ui-plus": "^0.0.1-beta.15",
"js-base64": "^3.7.2",
"js-md5": "^0.7.3",
"md-editor-v3": "^2.7.2",
"monaco-editor": "^0.34.0",
"nprogress": "^0.2.0",
"pinia": "^2.0.12",

25
frontend/src/api/interface/setting.ts

@ -5,6 +5,7 @@ export namespace Setting {
userName: string;
password: string;
email: string;
systemVersion: string;
sessionTimeout: number;
localTime: string;
@ -46,16 +47,36 @@ export namespace Setting {
code: string;
}
export interface SnapshotCreate {
from: string;
description: string;
backupType: string;
}
export interface SnapshotRecover {
id: number;
isNew: boolean;
reDownload: boolean;
}
export interface SnapshotInfo {
id: number;
name: string;
from: string;
description: string;
backupType: string;
status: string;
message: string;
createdAt: DateTimeFormats;
version: string;
interruptStep: string;
recoverStatus: string;
recoverMessage: string;
lastRecoveredAt: string;
rollbackStatus: string;
rollbackMessage: string;
lastRollbackedAt: string;
}
export interface UpgradeInfo {
newVersion: string;
tag: string;
releaseNote: string;
createdAt: string;
publishedAt: string;
}
}

14
frontend/src/api/modules/setting.ts

@ -42,6 +42,20 @@ export const bindMFA = (param: Setting.MFABind) => {
export const snapshotCreate = (param: Setting.SnapshotCreate) => {
return http.post(`/settings/snapshot`, param);
};
export const snapshotDelete = (param: { ids: number[] }) => {
return http.post(`/settings/snapshot/del`, param);
};
export const snapshotRecover = (param: Setting.SnapshotRecover) => {
return http.post(`/settings/snapshot/recover`, param);
};
export const snapshotRollback = (param: Setting.SnapshotRecover) => {
return http.post(`/settings/snapshot/rollback`, param);
};
export const searchSnapshotPage = (param: ReqPage) => {
return http.post<ResPage<Setting.SnapshotInfo>>(`/settings/snapshot/search`, param);
};
// upgrade
export const loadUpgradeInfo = () => {
return http.get<Setting.UpgradeInfo>(`/settings/upgrade`);
};

23
frontend/src/lang/modules/en.ts

@ -27,6 +27,7 @@ export default {
log: 'Log',
back: 'Back',
recover: 'Recover',
retry: 'Retry',
upload: 'Upload',
download: 'Download',
init: 'Init',
@ -51,6 +52,7 @@ export default {
records: 'Records',
group: 'Group',
createdAt: 'Creation Time',
publishedAt: 'Publish Time',
date: 'Date',
updatedAt: 'Update Time',
operate: 'Operations',
@ -737,6 +739,27 @@ export default {
mfaHelper3: 'Enter six digits from the app',
snapshot: 'Snapshot',
recoverDetail: 'Recover detail',
createSnapshot: 'Create snapshot',
recover: 'Recover',
noRecoverRecord: 'No recovery record has been recorded',
lastRecoverAt: 'Last recovery time',
lastRollbackAt: 'Last rollback time',
noRollbackRecord: 'No rollback record has been recorded',
reDownload: 'Download the backup file again',
recoverRecord: 'Recover record',
recoverHelper:
'The recovery is about to start from snapshot {0}, and the recovery needs to restart docker and 1panel service, do you want to continue?',
rollback: 'Rollback',
rollbackHelper:
'This recovery is about to be rolled back, which will replace all the files recovered this time. In the process, docker and 1panel services may need to be restarted. Do you want to continue?',
upgrade: 'Upgrade',
newVersion: 'NewVersion',
upgradeCheck: 'Check for updates',
tag: 'Tag',
upgradeNotes: 'Release note',
upgradeNow: 'Upgrade now',
enableMonitor: 'Enable',
storeDays: 'Expiration time (day)',

27
frontend/src/lang/modules/zh.ts

@ -28,6 +28,7 @@ export default {
log: '日志',
back: '返回',
recover: '恢复',
retry: '重试',
upload: '上传',
download: '下载',
init: '初始化',
@ -49,9 +50,11 @@ export default {
status: '状态',
statusSuccess: '成功',
statusFailed: '失败',
statusWaiting: '进行中...',
records: '任务输出',
group: '分组',
createdAt: '创建时间',
publishedAt: '发布时间',
date: '时间',
updatedAt: '更新时间',
operate: '操作',
@ -726,6 +729,30 @@ export default {
path: '路径',
snapshot: '快照',
recoverDetail: '恢复详情',
createSnapshot: '新建快照',
recover: '恢复',
noRecoverRecord: '暂无恢复记录',
lastRecoverAt: '上次恢复时间',
lastRollbackAt: '上次回滚时间',
noRollbackRecord: '暂无回滚记录',
reDownload: '重新下载备份文件',
statusAll: '全部',
statusSuccess: '成功',
statusFailed: '失败',
versionChange: '版本变化',
snapshotFrom: '快照存储位置',
recoverHelper: '即将从快照 {0} 开始恢复恢复需要重启 docker 以及 1panel 服务是否继续',
rollback: '回滚',
rollbackHelper:
'即将回滚本次恢复回滚将替换所有本次恢复的文件过程中可能需要重启 docker 以及 1panel 服务是否继续',
upgrade: '升级',
newVersion: '新版本',
upgradeCheck: '检查更新',
tag: '标签',
upgradeNotes: '更新内容',
upgradeNow: '立即更新',
safe: '安全',
panelPort: '面板端口',

53
frontend/src/views/setting/about/index.vue

@ -12,7 +12,12 @@
<img style="width: 80px" src="@/assets/images/ko_image.png" />
</div>
<h3>{{ $t('setting.description') }}</h3>
<h3>v1.0.0</h3>
<h3>
{{ version }}
<el-button type="primary" link @click="onLoadUpgradeInfo">
{{ $t('setting.upgradeCheck') }}
</el-button>
</h3>
<div style="margin-top: 10px">
<el-link @click="toGithub">
<svg-icon style="font-size: 7px; margin-bottom: 3px" iconName="p-huaban88"></svg-icon>
@ -33,11 +38,47 @@
</div>
</div>
</el-card>
<el-drawer :key="refresh" v-model="drawerShow" size="50%">
<el-form label-width="120px">
<el-form-item :label="$t('setting.newVersion')">
<el-tag>{{ upgradeInfo.newVersion }}</el-tag>
</el-form-item>
<el-form-item :label="$t('setting.tag')">
<el-tag>{{ upgradeInfo.tag }}</el-tag>
</el-form-item>
<el-form-item :label="$t('setting.upgradeNotes')">
<MdEditor style="height: 450px" v-model="upgradeInfo.releaseNote" previewOnly />
</el-form-item>
<el-form-item :label="$t('commons.table.createdAt')">
<el-tag>{{ upgradeInfo.createdAt }}</el-tag>
</el-form-item>
<el-form-item :label="$t('commons.table.publishedAt')">
<el-tag>{{ upgradeInfo.publishedAt }}</el-tag>
</el-form-item>
<el-form-item>
<el-button type="primary">{{ $t('setting.upgradeNow') }}</el-button>
</el-form-item>
</el-form>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { getSettingInfo, loadUpgradeInfo } from '@/api/modules/setting';
import Submenu from '@/views/setting/index.vue';
import { onMounted, ref } from 'vue';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
const version = ref();
const upgradeInfo = ref();
const drawerShow = ref();
const refresh = ref();
const search = async () => {
const res = await getSettingInfo();
version.value = res.data.systemVersion;
};
const toGithub = () => {
window.open('https://github.com/1Panel-dev/1Panel', '_blank');
@ -51,4 +92,14 @@ const toTalk = () => {
const toGithubStar = () => {
window.open('https://github.com/1Panel-dev/1Panel', '_blank');
};
const onLoadUpgradeInfo = async () => {
const res = await loadUpgradeInfo();
upgradeInfo.value = res.data;
drawerShow.value = true;
};
onMounted(() => {
search();
});
</script>

1
frontend/src/views/setting/index.vue

@ -58,6 +58,7 @@ const handleChange = (val: string) => {
break;
case 'snapshot':
routerTo('/setting/snapshot');
break;
}
};
</script>

8
frontend/src/views/setting/safe/index.vue

@ -127,8 +127,6 @@ import i18n from '@/lang';
import { Rules } from '@/global/form-rules';
import { dateFromat } from '@/utils/util';
const emit = defineEmits(['search']);
const loading = ref(false);
const form = reactive({
serverPort: '',
@ -207,7 +205,7 @@ const handleMFA = async () => {
await updateSetting({ key: 'MFAStatus', value: 'disable' })
.then(() => {
loading.value = false;
emit('search');
search();
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
@ -221,7 +219,7 @@ const onBind = async () => {
await bindMFA({ code: mfaCode.value, secret: otp.secret })
.then(() => {
loading.value = false;
emit('search');
search();
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
isMFAShow.value = false;
})
@ -249,7 +247,7 @@ const submitTimeout = async (formEl: FormInstance | undefined) => {
await updateSetting({ key: 'ExpirationDays', value: timeoutForm.days + '' })
.then(() => {
loading.value = false;
emit('search');
search();
loadTimeOut();
form.expirationTime = dateFromat(0, 0, time);
timeoutVisiable.value = false;

137
frontend/src/views/setting/snapshot/index.vue

@ -1,42 +1,70 @@
<template>
<div>
<div v-loading="loading">
<Submenu activeName="snapshot" />
<el-card style="margin-top: 20px">
<el-card v-if="!isRecordShow" style="margin-top: 20px">
<ComplexTable :pagination-config="paginationConfig" v-model:selects="selects" :data="data" @search="search">
<template #toolbar>
<el-button type="primary" icon="Plus" @click="onCreate()">
{{ $t('commons.button.create') }}
{{ $t('setting.createSnapshot') }}
</el-button>
<!-- <el-button type="danger" plain :disabled="selects.length === 0" @click="batchDelete(null)">
<el-button type="danger" plain :disabled="selects.length === 0" @click="batchDelete(null)">
{{ $t('commons.button.delete') }}
</el-button> -->
</el-button>
</template>
<el-table-column type="selection" fix />
<el-table-column :label="$t('commons.table.name')" min-width="100" prop="name" fix />
<el-table-column
show-overflow-tooltip
:label="$t('commons.table.name')"
min-width="100"
prop="name"
fix
/>
<el-table-column
:label="$t('commons.table.description')"
min-width="150"
show-overflow-tooltip
prop="description"
/>
<el-table-column :label="$t('setting.backupAccount')" min-width="150" prop="backupAccount" />
<el-table-column :label="$t('commons.table.status')" min-width="80" prop="status">
<el-table-column :label="$t('setting.backupAccount')" min-width="150" prop="from" />
<el-table-column :label="$t('setting.backup')" min-width="80" prop="status">
<template #default="{ row }">
<el-tag v-if="row.status === 'Success'" type="success">
{{ $t('commons.table.statusSuccess') }}
</el-tag>
<el-tooltip v-else class="box-item" effect="dark" :content="row.message" placement="top-start">
<el-tag v-if="row.status === 'Waiting'" type="info">
{{ $t('commons.table.statusWaiting') }}
</el-tag>
<el-tooltip
v-if="row.status === 'Failed'"
class="box-item"
effect="dark"
:content="row.message"
placement="top-start"
>
<el-tag type="danger">{{ $t('commons.table.statusFailed') }}</el-tag>
</el-tooltip>
</template>
</el-table-column>
<fu-table-operations :buttons="buttons" :label="$t('commons.table.operate')" fix />
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFromat"
show-overflow-tooltip
/>
<fu-table-operations
width="200px"
:ellipsis="10"
:buttons="buttons"
:label="$t('commons.table.operate')"
fix
/>
</ComplexTable>
</el-card>
<el-dialog v-model="dialogVisiable" :title="$t('commons.button.create')" width="30%">
<el-form ref="snapRef" label-width="100px" :model="snapInfo" :rules="rules">
<el-form-item :label="$t('cronjob.target')" prop="backupType">
<el-select v-model="snapInfo.backupType" clearable>
<RecoverStatus ref="recoverStatusRef"></RecoverStatus>
<el-dialog v-model="dialogVisiable" :title="$t('setting.createSnapshot')" width="30%">
<el-form v-loading="loading" ref="snapRef" label-width="100px" :model="snapInfo" :rules="rules">
<el-form-item :label="$t('cronjob.target')" prop="from">
<el-select v-model="snapInfo.from" clearable>
<el-option
v-for="item in backupOptions"
:key="item.label"
@ -51,8 +79,10 @@
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisiable = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submitAddSnapshot(snapRef)">
<el-button :disabled="loading" @click="dialogVisiable = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button :disabled="loading" type="primary" @click="submitAddSnapshot(snapRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
@ -63,18 +93,21 @@
<script setup lang="ts">
import ComplexTable from '@/components/complex-table/index.vue';
import { snapshotCreate, searchSnapshotPage } from '@/api/modules/setting';
import { snapshotCreate, searchSnapshotPage, snapshotDelete } from '@/api/modules/setting';
import { onMounted, reactive, ref } from 'vue';
// import { useDeleteData } from '@/hooks/use-delete-data';
import type { ElForm } from 'element-plus';
import { dateFromat } from '@/utils/util';
import { useDeleteData } from '@/hooks/use-delete-data';
import { ElForm } from 'element-plus';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { ElMessage } from 'element-plus';
import { Setting } from '@/api/interface/setting';
import Submenu from '@/views/setting/index.vue';
import RecoverStatus from '@/views/setting/snapshot/status/index.vue';
import { getBackupList } from '@/api/modules/backup';
import { loadBackupName } from '../helper';
const loading = ref(false);
const data = ref();
const selects = ref<any>([]);
const paginationConfig = reactive({
@ -83,17 +116,18 @@ const paginationConfig = reactive({
total: 0,
});
const recoverStatusRef = ref();
const isRecordShow = ref();
const backupOptions = ref();
type FormInstance = InstanceType<typeof ElForm>;
const snapRef = ref<FormInstance>();
const rules = reactive({
backupType: [Rules.requiredSelect],
from: [Rules.requiredSelect],
});
let snapInfo = reactive<Setting.SnapshotCreate>({
from: '',
description: '',
backupType: '',
});
const dialogVisiable = ref<boolean>(false);
@ -107,10 +141,17 @@ const submitAddSnapshot = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
await snapshotCreate(snapInfo);
dialogVisiable.value = false;
search();
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
loading.value = true;
await snapshotCreate(snapInfo)
.then(() => {
loading.value = false;
dialogVisiable.value = false;
search();
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
};
@ -124,18 +165,18 @@ const loadBackups = async () => {
}
};
// const batchDelete = async (row: Snapshot.SnapshotInfo | null) => {
// let ids: Array<number> = [];
// if (row === null) {
// selects.value.forEach((item: Snapshot.SnapshotInfo) => {
// ids.push(item.id);
// });
// } else {
// ids.push(row.id);
// }
// await useDeleteData(deleteSnapshot, { ids: ids }, 'commons.msg.delete');
// search();
// };
const batchDelete = async (row: Setting.SnapshotInfo | null) => {
let ids: Array<number> = [];
if (row === null) {
selects.value.forEach((item: Setting.SnapshotInfo) => {
ids.push(item.id);
});
} else {
ids.push(row.id);
}
await useDeleteData(snapshotDelete, { ids: ids }, 'commons.msg.delete');
search();
};
function restForm() {
if (snapRef.value) {
@ -143,11 +184,21 @@ function restForm() {
}
}
const buttons = [
// {
// label: i18n.global.t('commons.button.delete'),
// icon: 'Delete',
// click: batchDelete,
// },
{
label: i18n.global.t('commons.button.recover'),
icon: 'RefreshLeft',
click: (row: any) => {
recoverStatusRef.value.acceptParams({ snapInfo: row });
},
disabled: (row: any) => {
return !(row.status === 'Success');
},
},
{
label: i18n.global.t('commons.button.delete'),
icon: 'Delete',
click: batchDelete,
},
];
const search = async () => {

242
frontend/src/views/setting/snapshot/status/index.vue

@ -0,0 +1,242 @@
<template>
<div v-loading="loading">
<el-drawer v-model="drawerVisiable" :title="$t('setting.recoverDetail')">
<el-form label-width="120px">
<el-card>
<template #header>
<div class="card-header">
<span>{{ $t('setting.recover') }}</span>
</div>
</template>
<div v-if="!snapInfo.recoverStatus">
<div v-if="snapInfo.lastRecoveredAt">
<el-form-item :label="$t('commons.table.status')">
<el-tag type="success">
{{ $t('commons.table.statusSuccess') }}
</el-tag>
<el-button @click="recoverSnapshot(true)" style="margin-left: 10px" type="primary">
{{ $t('setting.recover') }}
</el-button>
</el-form-item>
<el-form-item :label="$t('setting.lastRecoverAt')">
{{ snapInfo.lastRecoveredAt }}
</el-form-item>
</div>
<div v-else>
<el-form-item>
<el-tag type="info">
{{ $t('setting.noRecoverRecord') }}
</el-tag>
</el-form-item>
<el-form-item>
<el-button @click="recoverSnapshot(true)" type="primary">
{{ $t('setting.recover') }}
</el-button>
</el-form-item>
</div>
</div>
<div v-else>
<el-form-item :label="$t('commons.table.status')">
<el-tag type="danger" v-if="snapInfo.recoverStatus === 'Failed'">
{{ $t('commons.table.statusFailed') }}
</el-tag>
<el-tag type="success" v-if="snapInfo.recoverStatus === 'Success'">
{{ $t('commons.table.statusSuccess') }}
</el-tag>
<el-tag type="info" v-if="snapInfo.recoverStatus === 'Waiting'">
{{ $t('commons.table.statusWaiting') }}
</el-tag>
<el-button
style="margin-left: 15px"
@click="recoverSnapshot(true)"
:disabled="snapInfo.recoverStatus !== 'Success'"
>
{{ $t('setting.recover') }}
</el-button>
</el-form-item>
<el-form-item :label="$t('setting.lastRecoverAt')" v-if="snapInfo.recoverStatus !== 'Waiting'">
{{ snapInfo.lastRecoveredAt }}
</el-form-item>
<div v-if="snapInfo.recoverStatus === 'Failed'">
<el-form-item :label="$t('commons.button.log')">
<span style="word-break: break-all; flex-wrap: wrap; word-wrap: break-word">
{{ snapInfo.recoverMessage }}
</span>
</el-form-item>
<el-form-item>
<el-button @click="dialogVisiable = true" type="primary">
{{ $t('commons.button.retry') }}
</el-button>
</el-form-item>
</div>
</div>
</el-card>
<el-card style="margin-top: 20px" v-if="snapInfo.recoverStatus === 'Failed'">
<template #header>
<div class="card-header">
<span>{{ $t('setting.rollback') }}</span>
</div>
</template>
<div v-if="!snapInfo.rollbackStatus">
<div v-if="snapInfo.lastRollbackedAt">
<el-form-item :label="$t('commons.table.status')">
<el-tag type="success">
{{ $t('commons.table.statusSuccess') }}
</el-tag>
<el-button @click="rollbackSnapshot" style="margin-left: 10px" type="primary">
{{ $t('setting.recover') }}
</el-button>
</el-form-item>
<el-form-item :label="$t('setting.lastRollbackAt')">
{{ snapInfo.lastRollbackedAt }}
</el-form-item>
</div>
<div v-else>
<el-form-item>
<el-tag type="info">
{{ $t('setting.noRollbackRecord') }}
</el-tag>
</el-form-item>
<el-form-item>
<el-button @click="rollbackSnapshot" type="primary">
{{ $t('setting.rollback') }}
</el-button>
</el-form-item>
</div>
</div>
<div v-else>
<el-form-item :label="$t('commons.table.status')">
<el-tag type="success" v-if="snapInfo.rollbackStatus === 'Success'">
{{ $t('commons.table.statusSuccess') }}
</el-tag>
<el-tag type="danger" v-if="snapInfo.rollbackStatus === 'Failed'">
{{ $t('commons.table.statusFailed') }}
</el-tag>
<el-tag type="info" v-if="snapInfo.rollbackStatus === 'Waiting'">
{{ $t('commons.table.statusWaiting') }}
</el-tag>
<el-button
style="margin-left: 15px"
:disabled="snapInfo.rollbackStatus !== 'Success'"
@click="rollbackSnapshot"
>
{{ $t('setting.rollback') }}
</el-button>
</el-form-item>
<el-form-item
:label="$t('setting.lastRollbackAt')"
v-if="snapInfo.rollbackStatus !== 'Waiting'"
>
{{ snapInfo.lastRollbackedAt }}
</el-form-item>
<div v-if="snapInfo.rollbackStatus === 'Failed'">
<el-form-item :label="$t('commons.button.log')">
<span style="word-break: break-all; flex-wrap: wrap; word-wrap: break-word">
{{ snapInfo.rollbackMessage }}
</span>
</el-form-item>
<el-form-item>
<el-button @click="rollbackSnapshot()" type="primary">
{{ $t('commons.button.retry') }}
</el-button>
</el-form-item>
</div>
</div>
</el-card>
</el-form>
</el-drawer>
<el-dialog v-model="dialogVisiable" :destroy-on-close="true" :close-on-click-modal="false" width="30%">
<template #header>
<div class="card-header">
<span>{{ $t('commons.button.retry') }}</span>
</div>
</template>
<div>
<span>{{ $t('setting.reDownload') }}</span>
<el-switch style="margin-left: 15px" v-model="reDownload" />
</div>
<div style="margin-top: 15px">
<span>{{ $t('setting.recoverHelper', [snapInfo.name]) }}</span>
</div>
<template #footer>
<span class="dialog-footer">
<el-button :disabled="loading" @click="dialogVisiable = false">
{{ $t('commons.button.cancel') }}
</el-button>
<el-button :disabled="loading" type="primary" @click="doRecover(false)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Setting } from '@/api/interface/setting';
import { ElMessage, ElMessageBox } from 'element-plus';
import i18n from '@/lang';
import { snapshotRecover, snapshotRollback } from '@/api/modules/setting';
const drawerVisiable = ref(false);
const snapInfo = ref();
const loading = ref();
const dialogVisiable = ref();
const reDownload = ref();
interface DialogProps {
snapInfo: Setting.SnapshotInfo;
}
const acceptParams = (params: DialogProps): void => {
snapInfo.value = params.snapInfo;
drawerVisiable.value = true;
};
const doRecover = async (isNew: boolean) => {
loading.value = true;
await snapshotRecover({ id: snapInfo.value.id, isNew: isNew, reDownload: reDownload.value })
.then(() => {
loading.value = false;
dialogVisiable.value = false;
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
};
const recoverSnapshot = async (isNew: boolean) => {
ElMessageBox.confirm(i18n.global.t('setting.recoverHelper', [snapInfo.value.name]), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
doRecover(isNew);
});
};
const rollbackSnapshot = async () => {
ElMessageBox.confirm(i18n.global.t('setting.rollbackHelper'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
}).then(async () => {
loading.value = true;
await snapshotRollback({ id: snapInfo.value.id, isNew: false, reDownload: false })
.then(() => {
loading.value = false;
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
});
};
defineExpose({
acceptParams,
});
</script>

4
go.mod

@ -21,9 +21,8 @@ require (
github.com/go-sql-driver/mysql v1.6.0
github.com/gogf/gf v1.16.9
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/gorilla/csrf v1.7.1
github.com/google/go-github v17.0.0+incompatible
github.com/gorilla/websocket v1.5.0
github.com/gwatts/gin-adapter v1.0.0
github.com/jinzhu/copier v0.3.5
github.com/joho/godotenv v1.4.0
github.com/mholt/archiver/v4 v4.0.0-alpha.7
@ -95,7 +94,6 @@ require (
github.com/google/flatbuffers v1.12.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect

8
go.sum

@ -497,6 +497,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -526,13 +528,9 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@ -547,8 +545,6 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/gwatts/gin-adapter v1.0.0 h1:TsmmhYTR79/RMTsfYJ2IQvI1F5KZ3ZFJxuQSYEOpyIA=
github.com/gwatts/gin-adapter v1.0.0/go.mod h1:44AEV+938HsS0mjfXtBDCUZS9vONlF2gwvh8wu4sRYc=
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=

15
node_modules/.package-lock.json generated vendored

@ -0,0 +1,15 @@
{
"name": "1Panel",
"lockfileVersion": 2,
"requires": true,
"packages": {
"node_modules/md-editor-v3": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/md-editor-v3/-/md-editor-v3-2.7.2.tgz",
"integrity": "sha512-CyLG7yZhMyKplXO/MYIccpL0AOcnys74cMpbBG77rmXWlANAmzLrznUU++g6MohTv3DCRNTz+5Uh/w9h9P2sSA==",
"engines": {
"node": ">=12.0.0"
}
}
}
}

21
node_modules/md-editor-v3/LICENSE generated vendored

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 imzbf.cc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

779
node_modules/md-editor-v3/README.md generated vendored

@ -0,0 +1,779 @@
# 🎄 md-editor-v3
![](https://img.shields.io/github/package-json/v/imzbf/md-editor-v3) ![](https://img.shields.io/npm/dm/md-editor-v3) ![](https://img.shields.io/bundlephobia/min/md-editor-v3) ![](https://img.shields.io/github/license/imzbf/md-editor-v3) ![](https://img.shields.io/badge/ssr-%3E1.6.0-brightgreen)
English \| [中文](https://github.com/imzbf/md-editor-v3/blob/develop/README-CN.md)
Markdown editor for vue3, developed in `jsx` and `typescript`.
- Documentation and example: [Go](https://imzbf.github.io/md-editor-v3)
- Use it online: [Go](https://codesandbox.io/s/epic-bird-2znqo)
- The same series editor for react: [md-editor-rt](https://github.com/imzbf/md-editor-rt)
## ⭐ Features
- Toolbar, screenfull or screenfull in web pages and so on.
- Themes, Built-in default and dark themes.
- Shortcut key for editor.
- Beautify your content by `prettier`(only for markdown content, not the code and other text).
- Multi-language, build-in Chinese and English(default: Chinese).
- Upload picture, paste or clip the picture and upload it.
- Render article directly(no editor, no event listener, only preview of content).
- Theme of preview, `defalut`, `vuepress`, `github`, `cyanosis`, `mk-cute`, `smart-blue` styles(not identical). It can be customized also(Refer to example page).
- `mermaid`(>=1.8.0), `katex` mathematical formula(>=1.9.0).
- Customize the toolbar as you like.
## 📦 Install
```shell
yarn add md-editor-v3
```
Please install `@types/marked` in your typescript project if you want to configure `renderer`:
```shell
yarn add @types/marked -D
```
Install existing extension of language and theme of preview:
```shell
yarn add @vavt/md-editor-extension
```
For more ways to use or contribute, please refer to: [md-editor-extension](https://github.com/imzbf/md-editor-extension)
## 💡 Usage
```vue
<template>
<md-editor v-model="text" preview-only />
</template>
<script setup>
import { ref } from 'vue';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
const text = ref('# Hello Editor');
</script>
```
## 🗺 Preview
| Default theme | Dark theme | Preview only |
| --- | --- | --- |
| ![](https://imzbf.github.io/md-editor-v3/imgs/preview-light.png) | ![](https://imzbf.github.io/md-editor-v3/imgs/preview-dark.png) | ![](https://imzbf.github.io/md-editor-v3/imgs/preview-previewOnly.png) |
mark and emoji extensions
![mark and emoji extension](https://imzbf.github.io/md-editor-v3/imgs/mark_emoji.gif)
## 🎁 Apis
### 🔩 Props
| name | type | default | description |
| --- | --- | --- | --- |
| modelValue | `string` | '' | Markdown content, use `v-model` in vue template |
| theme | `'light' \| 'dark'` | 'light' | Editor theme |
| class | `string` | '' | |
| historyLength | `number` | 10 | The max length of history(if it is too big, editor will use more `RAM`) |
| pageFullscreen | `boolean` | false | Screenfull in web page |
| preview | `boolean` | true | Preview content in editor |
| htmlPreview | `boolean` | false | Preview html in editor |
| previewOnly | `boolean` | false | Only render preview of content, no toolbar, no editing area |
| language | `string` | 'zh-CN' | Build-in language('zh-CN','en-US') |
| toolbars | `Array<ToolbarNames \| number>` | [toolbars] | Show contents of toolbar, all keys<sup>see `toolbars` below</sup> |
| toolbarsExclude | `Array<ToolbarNames \| number>` | [] | Don't show contents of toolbar, all keys`toolbars` |
| noPrettier | `boolean` | false | Use prettier to beautify content or not |
| editorId | `string` | 'md-editor-v3' | Editor's id, it is used when there are more than two editors in the same page |
| tabWidth | `number` | 2 | One tab eq some spaces |
| showCodeRowNumber | `boolean` | false | Show row number for code block or not |
| previewTheme | `'default' \| 'github' \| 'vuepress' \| 'mk-cute' \| 'smart-blue' \| 'cyanosis'` | 'default' | Theme of preview, can be customized |
| style | `string \| CSSProperties` | {} | Inline style |
| tableShape | `[number, number]` | [6, 4] | Preset the size of the table, [columns, rows] |
| noMermaid | `boolean` | false | Use mermaid or not |
| placeholder | `string` | '' | |
| noKatex | `boolean` | false | Use katex or not |
| codeTheme | `'atom' \| 'a11y' \| 'github' \| 'gradient' \| 'kimbie' \| 'paraiso' \| 'qtcreator' \| 'stackoverflow'` | 'atom' | Highlight code style, can be customized also |
| markedHeadingId | `(text: string, level: number, index: number) => string` | (text) => text | H1-H6 `ID` generator |
| sanitize | `(html: string) => string` | (html) => html | Sanitize the html, prevent XSS |
| footers | `Array<'markdownTotal' \| '=' \| 'scrollSwitch' \| number>` | ['markdownTotal', '=', 'scrollSwitch'] | Show contents of footer, they are divided by `'='`. Set it to `[]` to hidden footer |
| scrollAuto | `boolean` | true | Scroll default setting |
| noIconfont | `boolean` | false | Not append iconfont script, [download](https://at.alicdn.com/t/c/font_2605852_gymddm8qwtd.js) and import it by yourself |
| formatCopiedText | `(text: string) => string` | (text: string) => text | Format copied code |
| noUploadImg | `boolean` | false | Not show the entrance to upload pictures |
| codeStyleReverse | `boolean` | true | Code style will be reversed to dark while code block of the theme has a dark background |
| codeStyleReverseList | `Array<string>` | ['default', 'mk-cute'] | Themes to be reversed |
| autoFocus | `boolean` | false | same as `autofocus` in native textarea |
| disabled | `boolean` | false | same as `disabled` in native textarea |
| readOnly | `boolean` | false | same as `readonly` in native textarea |
| maxLength | `number` | | same as `maxlength` in native textarea |
| autoDetectCode | `boolean` | false | auto detect the type of pasted code, only support that copied from `vscode` |
<details>
<summary>『toolbars』</summary>
```js
[
'bold',
'underline',
'italic',
'-',
'strikeThrough',
'title',
'sub',
'sup',
'quote',
'unorderedList',
'orderedList',
'task', // ^2.4.0
'-',
'codeRow',
'code',
'link',
'image',
'table',
'mermaid',
'katex',
'-',
'revoke',
'next',
'save',
'=',
'pageFullscreen',
'fullscreen',
'preview',
'htmlPreview',
'catalog',
'github'
];
```
</details>
> After v1.6.0, You can sort the toolbar as you like, split tools by `'-'`, the left and right toolbars are divided by `'='`
> After v1.10.0, you can customize the toolbar. To display them, put index of `defToolbars` into `toolbars`(this is not standard), for more usage, please refer to [docs](https://imzbf.github.io/md-editor-v3/docs).
<details>
<summary>『StaticTextDefaultValue』</summary>
Expand language, you need to replace all the content here:
```ts
export interface ToolbarTips {
bold?: string;
underline?: string;
italic?: string;
strikeThrough?: string;
title?: string;
sub?: string;
sup?: string;
quote?: string;
unorderedList?: string;
orderedList?: string;
task?: string; // ^2.4.0
codeRow?: string;
code?: string;
link?: string;
image?: string;
table?: string;
mermaid?: string;
katex?: string;
revoke?: string;
next?: string;
save?: string;
prettier?: string;
pageFullscreen?: string;
fullscreen?: string;
catalog?: string;
preview?: string;
htmlPreview?: string;
github?: string;
'-'?: string;
'='?: string;
}
export interface StaticTextDefaultValue {
// Toolbar hover tips(html title)
toolbarTips?: ToolbarTips;
// H1-H6 dropdown menu item
titleItem?: {
h1?: string;
h2?: string;
h3?: string;
h4?: string;
h5?: string;
h6?: string;
};
imgTitleItem?: {
link: string;
upload: string;
clip2upload: string;
};
// The modal tips of add link or upload picture
linkModalTips?: {
linkTitle?: string;
imageTitle?: string;
descLabel?: string;
descLabelPlaceHolder?: string;
urlLabel?: string;
urlLabelPlaceHolder?: string;
buttonOK?: string;
};
// The modal tips of clip the picture, v1.2.0
clipModalTips?: {
title?: string;
buttonUpload?: string;
};
copyCode?: {
text?: string;
successTips?: string;
failTips?: string;
};
mermaid?: {
flow?: string;
sequence?: string;
gantt?: string;
class?: string;
state?: string;
pie?: string;
relationship?: string;
journey?: string;
};
katex?: {
// formula inline
inline: string;
// formula block
block: string;
};
footer?: {
markdownTotal: string;
scrollAuto: string;
};
}
```
</details>
## 🤱🏼 Expose
After 2.5.0, Editor exposes several methods on the instance, used to get or change the internal status of the editor.
```vue
<template>
<md-editor ref="editorRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue';
import MdEditor from 'md-editor-v3';
import type { ExposeParam } from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
const editorRef = ref<ExposeParam>();
onMounted(() => {
editorRef.value?.on('catalog', console.log);
editorRef.value?.toggleCatalog(true);
});
</script>
```
> Switched to the opposite status, if toggle without input parameter.
### 👂🏼 on
Get the internal state of the editor, including pageFullscreen, fullscreen, preview, htmlPreview, catalog, etc.
- pageFullscreen
```js
editorRef.value?.on('pageFullscreen', (status) => console.log(status));
```
- fullscreen
```js
editorRef.value?.on('fullscreen', (status) => console.log(status));
```
- preview
```js
editorRef.value?.on('preview', (status) => console.log(status));
```
- htmlPreview
```js
editorRef.value?.on('htmlPreview', (status) => console.log(status));
```
- catalog
```js
editorRef.value?.on('catalog', (status) => console.log(status));
```
### 💻 togglePageFullscreen
Toggle status of fullscreen within the page.
```js
editorRef.value?.togglePageFullscreen(true);
```
### 🖥 toggleFullscreen
Toggle status of fullscreen widthin browser.
```js
editorRef.value?.toggleFullscreen(true);
```
### 📖 togglePreview
Toggle status of preview.
```js
editorRef.value?.togglePreview(true);
```
### 📼 toggleHtmlPreview
Toggle status of htmlPreview.
```js
editorRef.value?.toggleHtmlPreview(true);
```
### 🧬 toggleCatalog
Toggle status of catalog.
```js
editorRef.value?.toggleCatalog(true);
```
### 💾 triggerSave
```js
editorRef.value?.triggerSave();
```
### 💉 insert
Manually insert content into textarea.
```js
/**
* @params selectedText
*/
editorRef.value?.insert((selectedText) => {
/**
* @return targetValue Content to be inserted
* @return select Automatically select content
* @return deviationStart Start position of the selected content
* @return deviationEnd End position of the selected content
*/
return {
targetValue: `${selectedText}`,
select: true,
deviationStart: 0,
deviationEnd: 0
};
});
```
For more examples, refer to source code of [extension component](https://github.com/imzbf/md-editor-v3/blob/dev-docs/src/components/MarkExtension/index.vue)
### 🎯 focus
focus the textarea.
```js
editorRef.value?.focus();
```
### 🎍 Slots
| name | type | default | description |
| --- | --- | --- | --- |
| defToolbars | `Array<DropdownToolbar \| NormalToolbar \| ModalToolbar>` | null | Custom toolbar in `DropdownToolbar`, `NormalToolbar` or `ModalToolbar` |
| defFooters | `Array<string \| VNode \| JSX.Element>` | null | Custom footer |
`NormalToolbar` example:
```vue
<template>
<md-editor>
<template #defToolbars>
<normal-toolbar title="mark" @onClick="handler">
<template #trigger>
<svg class="md-editor-icon" aria-hidden="true">
<use xlink:href="#md-editor-icon-mark"></use>
</svg>
</template>
</normal-toolbar>
</template>
</md-editor>
</template>
<script setup>
import MdEditor from 'md-editor-v3';
const NormalToolbar = MdEditor.NormalToolbar;
const handler = () => {
console.log('NormalToolbar clicked!');
};
</script>
```
### 🪢 Events
| name | param | description |
| --- | --- | --- |
| onChange | `value: string` | Content changed event(bind to `oninput` of `textarea`) |
| onSave | `value: string, html: Promise<string>` | Save content event, `ctrl+s`and click button will be triggered also |
| onUploadImg | `files: Array<File>, callback: (urls: Array<string>) => void` | Upload picture event, when picture is uploading the modal will not close, please provide right urls to the callback function |
| onHtmlChanged | `html: string` | Compile markdown successful event, you can use it to get the html code |
| onGetCatalog | `list: Array<HeadList>` | Get catalog of article |
| onError | `err: { name: string; message: string }` | Catch run-time error, `Cropper`, `fullscreen` and `prettier` are used when they are not loaded |
## 💴 Config Editor
Use `MdEditor.config(option: ConfigOption)` to reconfigure `renderer`.
- markedRenderer: `(renderer: RewriteRenderer) => RewriteRenderer`
Open target page in a new browser window:
```js
MdEditor.config({
markedRenderer(renderer) {
renderer.link = (href, title, text) => {
return `<a href="${href}" title="${title}" target="_blank">${text}</a>`;
};
return renderer;
}
});
```
> Reference: https://marked.js.org/using_pro#renderer, RewriteRenderer extends Renderer and rewrites heading, now provides index as the fifth parameter.
- markedExtensions: `Array<marked.TokenizerExtension & marked.RendererExtension>`
```js
import MdEditor from 'md-editor-v3';
MdEditor.config({
markedExtensions: [your extension]
});
```
> Reference: https://marked.js.org/using_pro#extensions
- markedOptions: `marked.MarkedOptions`
Do not render `<br>` on a single line break:
```js
import MdEditor from 'md-editor-v3';
MdEditor.config({
markedOptions: { breaks: false }
});
```
> Reference: https://marked.js.org/using_advanced#options
- editorConfig: Add more languages, reset `mermaid` template or delay rendering time:
```js
import MdEditor from 'md-editor-v3';
MdEditor.config({
editorConfig: {
languageUserDefined: { lang: StaticTextDefaultValue },
mermaidTemplate: {
flow: `flow tempalte`,
...more
},
// Default 500ms. It is set to 0ms when preview only and not set.
renderDelay: 500
}
});
```
- editorExtensions: Config some dependency libraries, like highlight..
```typescript
import MdEditor from 'md-editor-v3';
MdEditor.config({
editorExtensions: { iconfont: 'https://xxx.cc' }
});
```
<details>
<summary>『EditorExtensions』</summary>
```ts
import MdEditor from 'md-editor-v3';
interface EditorExtensions {
highlight?: {
instance?: any;
js?: string;
css?: {
[key: string]: {
light: string;
dark: string;
};
};
};
prettier?: {
standaloneJs?: string;
parserMarkdownJs?: string;
};
cropper?: {
instance?: any;
js?: string;
css?: string;
};
iconfont?: string;
screenfull?: {
instance?: any;
js?: string;
};
mermaid?: {
instance?: any;
js?: string;
};
katex?: {
instance?: any;
js?: string;
css?: string;
};
}
```
</details>
## 🪡 Shortcut Key
_Pay attention: shortcut keys are only available when the textarea is focused!_
| key | function | description |
| --- | --- | --- |
| TAB | insert space | Insert space, the length eq `tabWidth`, default: 2, support multiline |
| SHIFT + TAB | delete space, setting is the same as Tab | |
| CTRL + C | copy | When selected, copy the selected content. When not selected, copy the content of the current line |
| CTRL + X | shear | When selected, cut the selected content. When not selected, cut the current line |
| CTRL + D | delete | When selected, delete the selected content. When not selected, delete the current line |
| CTRL + S | save | Trigger `onSave` event |
| CTRL + B | bold text | `**bold**` |
| CTRL + U | underline | `<u>underline</u>` |
| CTRL + I | italic | `*italic*` |
| CTRL + 1-6 | h1-h6 | `# title` |
| CTRL + ↑ | superscript | `<sup>superscript</sup>` |
| CTRL + ↓ | subscript | `<sub>subscript</sub>` |
| CTRL + Q | quote | `> quote` |
| CTRL + O | ordered list | `1. ordered list` |
| CTRL + L | link | `[link](https://github.com/imzbf/md-editor-v3)` |
| CTRL + Z | withdraw | Withdraw history in editor, not the function of system |
| CTRL + SHIFT + S | line-through | `~line-through~` |
| CTRL + SHIFT + U | unordered list | `- unordered list` |
| CTRL + SHIFT + C | code block | |
| CTRL + SHIFT + I | picture | `![picture](https://imzbf.cc)` |
| CTRL + SHIFT + Z | forward | Forward history in editor, not the function of system |
| CTRL + SHIFT + F | Beautify | |
| CTRL + ALT + C | code row | |
| CTRL + SHIFT + ALT + T | table | `\|table\|` |
## 🪤 Components
They are used as attributes of the editor component, eg: `Editor.DropdownToolbar`. For more examples, refer to [document](https://imzbf.github.io/md-editor-v3).
### 🐣 NormalToolbar
`Editor.NormalToolbar`
- **props**
- `title`: `string`, not necessary, title of toolbar.
- **events**
- `onClick`: `(e: MouseEvent) => void`, necessary.
- **slots**
- `trigger`: `string | JSX.Element`, necessary, it is usually an icon, which is displayed on the toolbar.
### 🐼 DropdownToolbar
`Editor.DropdownToolbar`
- **props**
- `title`: `string`, not necessary, title of toolbar.
- `visible`: `boolean`, necessary.
- **events**
- `onChange`: `(visible: boolean) => void`, necessary.
- **slots**
- `trigger`: `string | JSX.Element`, necessary, it is usually an icon, which is displayed on the toolbar.
- `overlay`: `string | JSX.Element`, necessary, content of dropdown box.
### 🦉 ModalToolbar
`Editor.ModalToolbar`
- **props**
- `title`: `string`, not necessary, title of toolbar.
- `modalTitle`: `string`, not necessary, title of the Modal.
- `visible`: `boolean`, necessary, visibility of Modal.
- `width`: `string`, not necessary, width of Modal, default `auto`.
- `height`: `string`, same as `width`.
- `showAdjust`: `boolean`, not necessary, visibility of fullscreen button.
- `isFullscreen`: `boolean`, necessary when `showAdjust = true`, status of fullscreen.
- **events**
- `onClick`: `() => void`, necessary.
- `onClose`: `() => void`, necessary, close event.
- `onAdjust`: `(val: boolean) => void`, fullscreen button click event.
- **slots**
- `trigger`: `string | JSX.Element`, necessary, it is usually an icon, which is displayed on the toolbar.
- `overlay`: `string | JSX.Element`, necessary, content of Modal.
### 🐻 MdCatalog
`Editor.MdCatalog`
- **props**
- `editorId`: `string`, necessary, same as editor's `editorId`, used to register listening events.
- `class`: `string`, not necessary.
- `markedHeadingId`: `MarkedHeadingId`, not necessary, same as editor.
- `scrollElement`: `string | HTMLElement`, not necessary, it is an element selector when its type is string. When `previewOnly` eq `true`, it is usually set to `document.documentElement`.
- `theme`: `'light' | 'dark'`, not necessary, provide it when you want to change theme online, it is the same as Editor `theme`.
- `offsetTop`: `number`, not necessary, highlight current item of catalogs when title is `offsetTop` pixels from the top, defalut 20.
- `scrollElementOffsetTop`: `number`, not necessary, offsetTop of the scroll container,defalut 0.
- **events**
- `onClick`: `(e: MouseEvent, t: TocItem) => void`, not necessary.
## 🗂 Examples
### 🎸 Jsx Template
```js
import { defineComponent, reactive } from 'vue';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
export default defineComponent({
setup() {
const md = reactive({
text: '# Hello Editor'
});
return () => (
<MdEditor modelValue={md.text} onChange={(value) => (md.text = value)} />
);
}
});
```
### 🥹 Upload Picture
> Tips: When you paste and upload GIF, it will upload a static picture. So you should upload it by file system!
```vue
<template>
<md-editor v-model="text" @onUploadImg="onUploadImg" />
</template>
<script setup>
import { ref } from 'vue';
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
const text = ref('# Hello Editor');
const onUploadImg = async (files, callback) => {
const res = await Promise.all(
files.map((file) => {
return new Promise((rev, rej) => {
const form = new FormData();
form.append('file', file);
axios
.post('/api/img/upload', form, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then((res) => rev(res))
.catch((error) => rej(error));
});
})
);
callback(res.map((item) => item.data.url));
};
</script>
```
### 🧙 Change Styles
```less
.css-vars(@isDark) {
--md-color: if(@isDark, #999, #222);
--md-hover-color: if(@isDark, #bbb, #000);
--md-bk-color: if(@isDark, #000, #fff);
--md-bk-color-outstand: if(@isDark, #111, #f6f6f6);
--md-bk-hover-color: if(@isDark, #1b1a1a, #f5f7fa);
--md-border-color: if(@isDark, #2d2d2d, #e6e6e6);
--md-border-hover-color: if(@isDark, #636262, #b9b9b9);
--md-border-active-color: if(@isDark, #777, #999);
--md-modal-mask: #00000073;
--md-scrollbar-bg-color: if(@isDark, #0f0f0f, #e2e2e2);
--md-scrollbar-thumb-color: if(@isDark, #2d2d2d, #0000004d);
--md-scrollbar-thumb-hover-color: if(@isDark, #3a3a3a, #00000059);
--md-scrollbar-thumb-avtive-color: if(@isDark, #3a3a3a, #00000061);
}
.md-editor {
.css-vars(false);
}
.md-editor-dark {
.css-vars(true);
}
```
Change background color in dark mode:
```css
.md-editor-dark {
--md-bk-color: #333 !important;
}
```

356
node_modules/md-editor-v3/lib/MdEditor/Editor.d.ts generated vendored

@ -0,0 +1,356 @@
declare const Editor: import("vue").DefineComponent<{
modelValue: {
type: import("vue").PropType<string>;
default: string;
};
theme: {
type: import("vue").PropType<import("./type").Themes>;
default: string;
};
class: {
type: StringConstructor;
default: string;
};
historyLength: {
type: import("vue").PropType<number>;
default: number;
};
onChange: {
type: import("vue").PropType<import("./type").ChangeEvent>;
};
onSave: {
type: import("vue").PropType<import("./type").SaveEvent>;
};
onUploadImg: {
type: import("vue").PropType<import("./type").UploadImgEvent>;
};
pageFullscreen: {
type: import("vue").PropType<boolean>;
default: boolean;
};
preview: {
type: import("vue").PropType<boolean>;
default: boolean;
};
htmlPreview: {
type: import("vue").PropType<boolean>;
default: boolean;
};
previewOnly: {
type: import("vue").PropType<boolean>;
default: boolean;
};
language: {
type: import("vue").PropType<string>;
default: string;
};
toolbars: {
type: import("vue").PropType<import("./type").ToolbarNames[]>;
default: string[];
};
toolbarsExclude: {
type: import("vue").PropType<import("./type").ToolbarNames[]>;
default: never[];
};
noPrettier: {
type: import("vue").PropType<boolean>;
default: boolean;
};
onHtmlChanged: {
type: import("vue").PropType<import("./type").HtmlChangedEvent>;
};
onGetCatalog: {
type: import("vue").PropType<import("./type").GetCatalogEvent>;
};
editorId: {
type: import("vue").PropType<string>;
default: string;
};
tabWidth: {
type: import("vue").PropType<number>;
default: number;
};
showCodeRowNumber: {
type: import("vue").PropType<boolean>;
default: boolean;
};
previewTheme: {
type: import("vue").PropType<string>;
default: string;
};
style: {
type: import("vue").PropType<string | import("vue").CSSProperties>;
default: () => {};
};
markedHeadingId: {
type: import("vue").PropType<import("./type").MarkedHeadingId>;
default: import("./type").MarkedHeadingId;
};
tableShape: {
type: import("vue").PropType<number[]>;
default: () => number[];
};
noMermaid: {
type: import("vue").PropType<boolean>;
default: boolean;
};
sanitize: {
type: import("vue").PropType<(html: string) => string>;
default: (html: string) => string;
};
placeholder: {
type: import("vue").PropType<string>;
default: string;
};
noKatex: {
type: import("vue").PropType<boolean>;
default: boolean;
};
defToolbars: {
type: import("vue").PropType<string | JSX.Element>;
};
onError: {
type: import("vue").PropType<import("./type").ErrorEvent>;
};
codeTheme: {
type: import("vue").PropType<string>;
default: string;
};
footers: {
type: import("vue").PropType<import("./type").Footers[]>;
default: import("./type").Footers[];
};
scrollAuto: {
type: import("vue").PropType<boolean>;
default: boolean;
};
defFooters: {
type: import("vue").PropType<string | JSX.Element>;
};
noIconfont: {
type: import("vue").PropType<boolean>;
};
formatCopiedText: {
type: import("vue").PropType<(text: string) => string>;
default: (text: string) => string;
};
noUploadImg: {
type: import("vue").PropType<boolean>;
};
codeStyleReverse: {
type: import("vue").PropType<boolean>;
default: boolean;
};
codeStyleReverseList: {
type: import("vue").PropType<string[]>;
default: string[];
};
autoFocus: {
type: import("vue").PropType<boolean>;
};
disabled: {
type: import("vue").PropType<boolean>;
};
readOnly: {
type: import("vue").PropType<boolean>;
};
maxLength: {
type: import("vue").PropType<number>;
};
autoDetectCode: {
type: import("vue").PropType<boolean>;
};
}, () => JSX.Element, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, import("vue").EmitsOptions, "onChange" | "onSave" | "onUploadImg" | "onHtmlChanged" | "onGetCatalog" | "onError" | "update:modelValue", import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
modelValue: {
type: import("vue").PropType<string>;
default: string;
};
theme: {
type: import("vue").PropType<import("./type").Themes>;
default: string;
};
class: {
type: StringConstructor;
default: string;
};
historyLength: {
type: import("vue").PropType<number>;
default: number;
};
onChange: {
type: import("vue").PropType<import("./type").ChangeEvent>;
};
onSave: {
type: import("vue").PropType<import("./type").SaveEvent>;
};
onUploadImg: {
type: import("vue").PropType<import("./type").UploadImgEvent>;
};
pageFullscreen: {
type: import("vue").PropType<boolean>;
default: boolean;
};
preview: {
type: import("vue").PropType<boolean>;
default: boolean;
};
htmlPreview: {
type: import("vue").PropType<boolean>;
default: boolean;
};
previewOnly: {
type: import("vue").PropType<boolean>;
default: boolean;
};
language: {
type: import("vue").PropType<string>;
default: string;
};
toolbars: {
type: import("vue").PropType<import("./type").ToolbarNames[]>;
default: string[];
};
toolbarsExclude: {
type: import("vue").PropType<import("./type").ToolbarNames[]>;
default: never[];
};
noPrettier: {
type: import("vue").PropType<boolean>;
default: boolean;
};
onHtmlChanged: {
type: import("vue").PropType<import("./type").HtmlChangedEvent>;
};
onGetCatalog: {
type: import("vue").PropType<import("./type").GetCatalogEvent>;
};
editorId: {
type: import("vue").PropType<string>;
default: string;
};
tabWidth: {
type: import("vue").PropType<number>;
default: number;
};
showCodeRowNumber: {
type: import("vue").PropType<boolean>;
default: boolean;
};
previewTheme: {
type: import("vue").PropType<string>;
default: string;
};
style: {
type: import("vue").PropType<string | import("vue").CSSProperties>;
default: () => {};
};
markedHeadingId: {
type: import("vue").PropType<import("./type").MarkedHeadingId>;
default: import("./type").MarkedHeadingId;
};
tableShape: {
type: import("vue").PropType<number[]>;
default: () => number[];
};
noMermaid: {
type: import("vue").PropType<boolean>;
default: boolean;
};
sanitize: {
type: import("vue").PropType<(html: string) => string>;
default: (html: string) => string;
};
placeholder: {
type: import("vue").PropType<string>;
default: string;
};
noKatex: {
type: import("vue").PropType<boolean>;
default: boolean;
};
defToolbars: {
type: import("vue").PropType<string | JSX.Element>;
};
onError: {
type: import("vue").PropType<import("./type").ErrorEvent>;
};
codeTheme: {
type: import("vue").PropType<string>;
default: string;
};
footers: {
type: import("vue").PropType<import("./type").Footers[]>;
default: import("./type").Footers[];
};
scrollAuto: {
type: import("vue").PropType<boolean>;
default: boolean;
};
defFooters: {
type: import("vue").PropType<string | JSX.Element>;
};
noIconfont: {
type: import("vue").PropType<boolean>;
};
formatCopiedText: {
type: import("vue").PropType<(text: string) => string>;
default: (text: string) => string;
};
noUploadImg: {
type: import("vue").PropType<boolean>;
};
codeStyleReverse: {
type: import("vue").PropType<boolean>;
default: boolean;
};
codeStyleReverseList: {
type: import("vue").PropType<string[]>;
default: string[];
};
autoFocus: {
type: import("vue").PropType<boolean>;
};
disabled: {
type: import("vue").PropType<boolean>;
};
readOnly: {
type: import("vue").PropType<boolean>;
};
maxLength: {
type: import("vue").PropType<number>;
};
autoDetectCode: {
type: import("vue").PropType<boolean>;
};
}>>, {
pageFullscreen: boolean;
preview: boolean;
htmlPreview: boolean;
modelValue: string;
theme: import("./type").Themes;
class: string;
historyLength: number;
previewOnly: boolean;
language: string;
toolbars: import("./type").ToolbarNames[];
toolbarsExclude: import("./type").ToolbarNames[];
noPrettier: boolean;
editorId: string;
tabWidth: number;
showCodeRowNumber: boolean;
previewTheme: string;
style: string | import("vue").CSSProperties;
markedHeadingId: import("./type").MarkedHeadingId;
tableShape: number[];
noMermaid: boolean;
sanitize: (html: string) => string;
placeholder: string;
noKatex: boolean;
codeTheme: string;
footers: import("./type").Footers[];
scrollAuto: boolean;
formatCopiedText: (text: string) => string;
codeStyleReverse: boolean;
codeStyleReverseList: string[];
}>;
export default Editor;

26
node_modules/md-editor-v3/lib/MdEditor/config.d.ts generated vendored

@ -0,0 +1,26 @@
import { CodeCss, Config, ConfigOption, Footers, StaticTextDefault } from './type';
export declare const prefix = "md-editor";
export declare const defaultEditorId = "md-editor-v3";
export declare const iconfontUrl = "https://at.alicdn.com/t/c/font_2605852_gymddm8qwtd.js";
export declare const cdnBase = "https://cdnjs.cloudflare.com/ajax/libs";
export declare const highlightUrl: string;
export declare const prettierUrl: {
main: string;
markdown: string;
};
export declare const cropperUrl: {
css: string;
js: string;
};
export declare const screenfullUrl: string;
export declare const allToolbar: string[];
export declare const allFooter: Array<Footers>;
export declare const staticTextDefault: StaticTextDefault;
export declare const mermaidUrl: string;
export declare const katexUrl: {
js: string;
css: string;
};
export declare const codeCss: CodeCss;
export declare const configOption: ConfigOption;
export declare const config: Config;

39
node_modules/md-editor-v3/lib/MdEditor/extensions/DropdownToolbar.d.ts generated vendored

@ -0,0 +1,39 @@
import { PropType, ExtractPropTypes } from 'vue';
declare const _default: import("vue").DefineComponent<{
title: {
type: PropType<string>;
default: string;
};
visible: {
type: PropType<boolean>;
};
trigger: {
type: PropType<string | JSX.Element>;
};
onChange: {
type: PropType<(visible: boolean) => void>;
};
overlay: {
type: PropType<string | JSX.Element>;
};
}, () => JSX.Element, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, import("vue").EmitsOptions, "onChange", import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<ExtractPropTypes<{
title: {
type: PropType<string>;
default: string;
};
visible: {
type: PropType<boolean>;
};
trigger: {
type: PropType<string | JSX.Element>;
};
onChange: {
type: PropType<(visible: boolean) => void>;
};
overlay: {
type: PropType<string | JSX.Element>;
};
}>>, {
title: string;
}>;
export default _default;

77
node_modules/md-editor-v3/lib/MdEditor/extensions/MdCatalog/CatalogLink.d.ts generated vendored

@ -0,0 +1,77 @@
import { PropType, ExtractPropTypes } from 'vue';
import { LooseRequired } from '@vue/shared';
import { TocItem } from './index';
import { MarkedHeadingId } from '../../type';
declare const catalogLinkProps: () => {
tocItem: {
type: PropType<TocItem>;
default: () => {};
};
markedHeadingId: {
type: PropType<MarkedHeadingId>;
default: () => () => void;
};
scrollElement: {
type: PropType<string | Element>;
default: string;
};
onClick: {
type: PropType<(e: MouseEvent, t: TocItem) => void>;
default: () => () => void;
};
scrollElementOffsetTop: {
type: PropType<number>;
default: number;
};
};
export declare type CatalogLinkProps = Readonly<LooseRequired<Readonly<ExtractPropTypes<ReturnType<typeof catalogLinkProps>>>>>;
declare const CatalogLink: import("vue").DefineComponent<{
tocItem: {
type: PropType<TocItem>;
default: () => {};
};
markedHeadingId: {
type: PropType<MarkedHeadingId>;
default: () => () => void;
};
scrollElement: {
type: PropType<string | Element>;
default: string;
};
onClick: {
type: PropType<(e: MouseEvent, t: TocItem) => void>;
default: () => () => void;
};
scrollElementOffsetTop: {
type: PropType<number>;
default: number;
};
}, () => JSX.Element, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, Record<string, any>, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<ExtractPropTypes<{
tocItem: {
type: PropType<TocItem>;
default: () => {};
};
markedHeadingId: {
type: PropType<MarkedHeadingId>;
default: () => () => void;
};
scrollElement: {
type: PropType<string | Element>;
default: string;
};
onClick: {
type: PropType<(e: MouseEvent, t: TocItem) => void>;
default: () => () => void;
};
scrollElementOffsetTop: {
type: PropType<number>;
default: number;
};
}>>, {
markedHeadingId: MarkedHeadingId;
onClick: (e: MouseEvent, t: TocItem) => void;
scrollElement: string | Element;
scrollElementOffsetTop: number;
tocItem: TocItem;
}>;
export default CatalogLink;

111
node_modules/md-editor-v3/lib/MdEditor/extensions/MdCatalog/index.d.ts generated vendored

@ -0,0 +1,111 @@
import { PropType, ExtractPropTypes } from 'vue';
import { MarkedHeadingId, Themes } from '../../type';
export interface TocItem {
text: string;
level: number;
index: number;
active: boolean;
children?: Array<TocItem>;
}
declare const MdCatalog: import("vue").DefineComponent<{
/**
* 编辑器的Id务必与需要绑定的编辑器Id相同
*/
editorId: {
type: PropType<string>;
};
class: {
type: StringConstructor;
default: string;
};
markedHeadingId: {
type: PropType<MarkedHeadingId>;
default: (text: string) => string;
};
/**
* 指定滚动的容器选择器需带上对应的符号默认预览框
* 元素必须定位
*
* 默认#md-editor-preview-wrapper
*/
scrollElement: {
type: PropType<string | HTMLElement>;
};
theme: {
type: PropType<Themes>;
default: string;
};
/**
* 高亮标题相对滚动容器顶部偏移量即距离该值时高亮当前目录菜单项
*
* 默认20px
*/
offsetTop: {
type: PropType<number>;
default: number;
};
/**
* 滚动区域的固定顶部高度
*
* 默认0
*/
scrollElementOffsetTop: {
type: PropType<number>;
default: number;
};
}, () => JSX.Element, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, "onClick"[], "onClick", import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<ExtractPropTypes<{
/**
* 编辑器的Id务必与需要绑定的编辑器Id相同
*/
editorId: {
type: PropType<string>;
};
class: {
type: StringConstructor;
default: string;
};
markedHeadingId: {
type: PropType<MarkedHeadingId>;
default: (text: string) => string;
};
/**
* 指定滚动的容器选择器需带上对应的符号默认预览框
* 元素必须定位
*
* 默认#md-editor-preview-wrapper
*/
scrollElement: {
type: PropType<string | HTMLElement>;
};
theme: {
type: PropType<Themes>;
default: string;
};
/**
* 高亮标题相对滚动容器顶部偏移量即距离该值时高亮当前目录菜单项
*
* 默认20px
*/
offsetTop: {
type: PropType<number>;
default: number;
};
/**
* 滚动区域的固定顶部高度
*
* 默认0
*/
scrollElementOffsetTop: {
type: PropType<number>;
default: number;
};
}>> & {
onOnClick?: ((...args: any[]) => any) | undefined;
}, {
theme: Themes;
class: string;
markedHeadingId: MarkedHeadingId;
offsetTop: number;
scrollElementOffsetTop: number;
}>;
export default MdCatalog;

96
node_modules/md-editor-v3/lib/MdEditor/extensions/ModalToolbar.d.ts generated vendored

@ -0,0 +1,96 @@
import { PropType, ExtractPropTypes } from 'vue';
declare const _default: import("vue").DefineComponent<{
title: {
type: PropType<string>;
default: string;
};
modalTitle: {
type: PropType<string>;
default: string;
};
visible: {
type: PropType<boolean>;
};
width: {
type: PropType<string>;
default: string;
};
height: {
type: PropType<string>;
default: string;
};
trigger: {
type: PropType<string | JSX.Element>;
};
onClick: {
type: PropType<() => void>;
};
onClose: {
type: PropType<() => void>;
};
/**
* 显示全屏按钮
*/
showAdjust: {
type: PropType<boolean>;
default: boolean;
};
isFullscreen: {
type: PropType<boolean>;
default: boolean;
};
onAdjust: {
type: PropType<(val: boolean) => void>;
};
}, () => JSX.Element, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, import("vue").EmitsOptions, "onClick" | "onClose" | "onAdjust", import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<ExtractPropTypes<{
title: {
type: PropType<string>;
default: string;
};
modalTitle: {
type: PropType<string>;
default: string;
};
visible: {
type: PropType<boolean>;
};
width: {
type: PropType<string>;
default: string;
};
height: {
type: PropType<string>;
default: string;
};
trigger: {
type: PropType<string | JSX.Element>;
};
onClick: {
type: PropType<() => void>;
};
onClose: {
type: PropType<() => void>;
};
/**
* 显示全屏按钮
*/
showAdjust: {
type: PropType<boolean>;
default: boolean;
};
isFullscreen: {
type: PropType<boolean>;
default: boolean;
};
onAdjust: {
type: PropType<(val: boolean) => void>;
};
}>>, {
title: string;
modalTitle: string;
width: string;
height: string;
showAdjust: boolean;
isFullscreen: boolean;
}>;
export default _default;

27
node_modules/md-editor-v3/lib/MdEditor/extensions/NormalToolbar.d.ts generated vendored

@ -0,0 +1,27 @@
import { PropType, ExtractPropTypes } from 'vue';
declare const _default: import("vue").DefineComponent<{
title: {
type: PropType<string>;
default: string;
};
trigger: {
type: PropType<string | JSX.Element>;
};
onClick: {
type: PropType<(e: MouseEvent) => void>;
};
}, () => JSX.Element, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, import("vue").EmitsOptions, "onClick", import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<ExtractPropTypes<{
title: {
type: PropType<string>;
default: string;
};
trigger: {
type: PropType<string | JSX.Element>;
};
onClick: {
type: PropType<(e: MouseEvent) => void>;
};
}>>, {
title: string;
}>;
export default _default;

811
node_modules/md-editor-v3/lib/MdEditor/index.d.ts generated vendored

@ -0,0 +1,811 @@
import { App } from 'vue';
import NormalToolbar from './extensions/NormalToolbar';
import DropdownToolbar from './extensions/DropdownToolbar';
import MdCatalog from './extensions/MdCatalog';
import ModalToolbar from './extensions/ModalToolbar';
import { config } from './config';
declare const _default: {
new (...args: any[]): {
$: import("vue").ComponentInternalInstance;
$data: {};
$props: Partial<{
pageFullscreen: boolean;
preview: boolean;
htmlPreview: boolean;
modelValue: string;
theme: import("./type").Themes;
class: string;
historyLength: number;
previewOnly: boolean;
language: string;
toolbars: import("./type").ToolbarNames[];
toolbarsExclude: import("./type").ToolbarNames[];
noPrettier: boolean;
editorId: string;
tabWidth: number;
showCodeRowNumber: boolean;
previewTheme: string;
style: string | import("vue").CSSProperties;
markedHeadingId: import("./type").MarkedHeadingId;
tableShape: number[];
noMermaid: boolean;
sanitize: (html: string) => string;
placeholder: string;
noKatex: boolean;
codeTheme: string;
footers: import("./type").Footers[];
scrollAuto: boolean;
formatCopiedText: (text: string) => string;
codeStyleReverse: boolean;
codeStyleReverseList: string[];
}> & Omit<Readonly<import("vue").ExtractPropTypes<{
modelValue: {
type: import("vue").PropType<string>;
default: string;
};
theme: {
type: import("vue").PropType<import("./type").Themes>;
default: string;
};
class: {
type: StringConstructor;
default: string;
};
historyLength: {
type: import("vue").PropType<number>;
default: number;
};
onChange: {
type: import("vue").PropType<import("./type").ChangeEvent>;
};
onSave: {
type: import("vue").PropType<import("./type").SaveEvent>;
};
onUploadImg: {
type: import("vue").PropType<import("./type").UploadImgEvent>;
};
pageFullscreen: {
type: import("vue").PropType<boolean>;
default: boolean;
};
preview: {
type: import("vue").PropType<boolean>;
default: boolean;
};
htmlPreview: {
type: import("vue").PropType<boolean>;
default: boolean;
};
previewOnly: {
type: import("vue").PropType<boolean>;
default: boolean;
};
language: {
type: import("vue").PropType<string>;
default: string;
};
toolbars: {
type: import("vue").PropType<import("./type").ToolbarNames[]>;
default: string[];
};
toolbarsExclude: {
type: import("vue").PropType<import("./type").ToolbarNames[]>;
default: never[];
};
noPrettier: {
type: import("vue").PropType<boolean>;
default: boolean;
};
onHtmlChanged: {
type: import("vue").PropType<import("./type").HtmlChangedEvent>;
};
onGetCatalog: {
type: import("vue").PropType<import("./type").GetCatalogEvent>;
};
editorId: {
type: import("vue").PropType<string>;
default: string;
};
tabWidth: {
type: import("vue").PropType<number>;
default: number;
};
showCodeRowNumber: {
type: import("vue").PropType<boolean>;
default: boolean;
};
previewTheme: {
type: import("vue").PropType<string>;
default: string;
};
style: {
type: import("vue").PropType<string | import("vue").CSSProperties>;
default: () => {};
};
markedHeadingId: {
type: import("vue").PropType<import("./type").MarkedHeadingId>;
default: import("./type").MarkedHeadingId;
};
tableShape: {
type: import("vue").PropType<number[]>;
default: () => number[];
};
noMermaid: {
type: import("vue").PropType<boolean>;
default: boolean;
};
sanitize: {
type: import("vue").PropType<(html: string) => string>;
default: (html: string) => string;
};
placeholder: {
type: import("vue").PropType<string>;
default: string;
};
noKatex: {
type: import("vue").PropType<boolean>;
default: boolean;
};
defToolbars: {
type: import("vue").PropType<string | JSX.Element>;
};
onError: {
type: import("vue").PropType<import("./type").ErrorEvent>;
};
codeTheme: {
type: import("vue").PropType<string>;
default: string;
};
footers: {
type: import("vue").PropType<import("./type").Footers[]>;
default: import("./type").Footers[];
};
scrollAuto: {
type: import("vue").PropType<boolean>;
default: boolean;
};
defFooters: {
type: import("vue").PropType<string | JSX.Element>;
};
noIconfont: {
type: import("vue").PropType<boolean>;
};
formatCopiedText: {
type: import("vue").PropType<(text: string) => string>;
default: (text: string) => string;
};
noUploadImg: {
type: import("vue").PropType<boolean>;
};
codeStyleReverse: {
type: import("vue").PropType<boolean>;
default: boolean;
};
codeStyleReverseList: {
type: import("vue").PropType<string[]>;
default: string[];
};
autoFocus: {
type: import("vue").PropType<boolean>;
};
disabled: {
type: import("vue").PropType<boolean>;
};
readOnly: {
type: import("vue").PropType<boolean>;
};
maxLength: {
type: import("vue").PropType<number>;
};
autoDetectCode: {
type: import("vue").PropType<boolean>;
};
}>> & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, "pageFullscreen" | "preview" | "htmlPreview" | "modelValue" | "theme" | "class" | "historyLength" | "previewOnly" | "language" | "toolbars" | "toolbarsExclude" | "noPrettier" | "editorId" | "tabWidth" | "showCodeRowNumber" | "previewTheme" | "style" | "markedHeadingId" | "tableShape" | "noMermaid" | "sanitize" | "placeholder" | "noKatex" | "codeTheme" | "footers" | "scrollAuto" | "formatCopiedText" | "codeStyleReverse" | "codeStyleReverseList">;
$attrs: {
[x: string]: unknown;
};
$refs: {
[x: string]: unknown;
};
$slots: Readonly<{
[name: string]: import("vue").Slot | undefined;
}>;
$root: import("vue").ComponentPublicInstance<{}, {}, {}, {}, {}, {}, {}, {}, false, import("vue").ComponentOptionsBase<any, any, any, any, any, any, any, any, any, {}>> | null;
$parent: import("vue").ComponentPublicInstance<{}, {}, {}, {}, {}, {}, {}, {}, false, import("vue").ComponentOptionsBase<any, any, any, any, any, any, any, any, any, {}>> | null;
$emit: ((event: string, ...args: any[]) => void) | ((event: string, ...args: any[]) => void);
$el: any;
$options: import("vue").ComponentOptionsBase<Readonly<import("vue").ExtractPropTypes<{
modelValue: {
type: import("vue").PropType<string>;
default: string;
};
theme: {
type: import("vue").PropType<import("./type").Themes>;
default: string;
};
class: {
type: StringConstructor;
default: string;
};
historyLength: {
type: import("vue").PropType<number>;
default: number;
};
onChange: {
type: import("vue").PropType<import("./type").ChangeEvent>;
};
onSave: {
type: import("vue").PropType<import("./type").SaveEvent>;
};
onUploadImg: {
type: import("vue").PropType<import("./type").UploadImgEvent>;
};
pageFullscreen: {
type: import("vue").PropType<boolean>;
default: boolean;
};
preview: {
type: import("vue").PropType<boolean>;
default: boolean;
};
htmlPreview: {
type: import("vue").PropType<boolean>;
default: boolean;
};
previewOnly: {
type: import("vue").PropType<boolean>;
default: boolean;
};
language: {
type: import("vue").PropType<string>;
default: string;
};
toolbars: {
type: import("vue").PropType<import("./type").ToolbarNames[]>;
default: string[];
};
toolbarsExclude: {
type: import("vue").PropType<import("./type").ToolbarNames[]>;
default: never[];
};
noPrettier: {
type: import("vue").PropType<boolean>;
default: boolean;
};
onHtmlChanged: {
type: import("vue").PropType<import("./type").HtmlChangedEvent>;
};
onGetCatalog: {
type: import("vue").PropType<import("./type").GetCatalogEvent>;
};
editorId: {
type: import("vue").PropType<string>;
default: string;
};
tabWidth: {
type: import("vue").PropType<number>;
default: number;
};
showCodeRowNumber: {
type: import("vue").PropType<boolean>;
default: boolean;
};
previewTheme: {
type: import("vue").PropType<string>;
default: string;
};
style: {
type: import("vue").PropType<string | import("vue").CSSProperties>;
default: () => {};
};
markedHeadingId: {
type: import("vue").PropType<import("./type").MarkedHeadingId>;
default: import("./type").MarkedHeadingId;
};
tableShape: {
type: import("vue").PropType<number[]>;
default: () => number[];
};
noMermaid: {
type: import("vue").PropType<boolean>;
default: boolean;
};
sanitize: {
type: import("vue").PropType<(html: string) => string>;
default: (html: string) => string;
};
placeholder: {
type: import("vue").PropType<string>;
default: string;
};
noKatex: {
type: import("vue").PropType<boolean>;
default: boolean;
};
defToolbars: {
type: import("vue").PropType<string | JSX.Element>;
};
onError: {
type: import("vue").PropType<import("./type").ErrorEvent>;
};
codeTheme: {
type: import("vue").PropType<string>;
default: string;
};
footers: {
type: import("vue").PropType<import("./type").Footers[]>;
default: import("./type").Footers[];
};
scrollAuto: {
type: import("vue").PropType<boolean>;
default: boolean;
};
defFooters: {
type: import("vue").PropType<string | JSX.Element>;
};
noIconfont: {
type: import("vue").PropType<boolean>;
};
formatCopiedText: {
type: import("vue").PropType<(text: string) => string>;
default: (text: string) => string;
};
noUploadImg: {
type: import("vue").PropType<boolean>;
};
codeStyleReverse: {
type: import("vue").PropType<boolean>;
default: boolean;
};
codeStyleReverseList: {
type: import("vue").PropType<string[]>;
default: string[];
};
autoFocus: {
type: import("vue").PropType<boolean>;
};
disabled: {
type: import("vue").PropType<boolean>;
};
readOnly: {
type: import("vue").PropType<boolean>;
};
maxLength: {
type: import("vue").PropType<number>;
};
autoDetectCode: {
type: import("vue").PropType<boolean>;
};
}>>, () => JSX.Element, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, import("vue").EmitsOptions, string, {
pageFullscreen: boolean;
preview: boolean;
htmlPreview: boolean;
modelValue: string;
theme: import("./type").Themes;
class: string;
historyLength: number;
previewOnly: boolean;
language: string;
toolbars: import("./type").ToolbarNames[];
toolbarsExclude: import("./type").ToolbarNames[];
noPrettier: boolean;
editorId: string;
tabWidth: number;
showCodeRowNumber: boolean;
previewTheme: string;
style: string | import("vue").CSSProperties;
markedHeadingId: import("./type").MarkedHeadingId;
tableShape: number[];
noMermaid: boolean;
sanitize: (html: string) => string;
placeholder: string;
noKatex: boolean;
codeTheme: string;
footers: import("./type").Footers[];
scrollAuto: boolean;
formatCopiedText: (text: string) => string;
codeStyleReverse: boolean;
codeStyleReverseList: string[];
}> & {
beforeCreate?: ((() => void) | (() => void)[]) | undefined;
created?: ((() => void) | (() => void)[]) | undefined;
beforeMount?: ((() => void) | (() => void)[]) | undefined;
mounted?: ((() => void) | (() => void)[]) | undefined;
beforeUpdate?: ((() => void) | (() => void)[]) | undefined;
updated?: ((() => void) | (() => void)[]) | undefined;
activated?: ((() => void) | (() => void)[]) | undefined;
deactivated?: ((() => void) | (() => void)[]) | undefined;
beforeDestroy?: ((() => void) | (() => void)[]) | undefined;
beforeUnmount?: ((() => void) | (() => void)[]) | undefined;
destroyed?: ((() => void) | (() => void)[]) | undefined;
unmounted?: ((() => void) | (() => void)[]) | undefined;
renderTracked?: (((e: import("vue").DebuggerEvent) => void) | ((e: import("vue").DebuggerEvent) => void)[]) | undefined;
renderTriggered?: (((e: import("vue").DebuggerEvent) => void) | ((e: import("vue").DebuggerEvent) => void)[]) | undefined;
errorCaptured?: (((err: unknown, instance: import("vue").ComponentPublicInstance<{}, {}, {}, {}, {}, {}, {}, {}, false, import("vue").ComponentOptionsBase<any, any, any, any, any, any, any, any, any, {}>> | null, info: string) => boolean | void) | ((err: unknown, instance: import("vue").ComponentPublicInstance<{}, {}, {}, {}, {}, {}, {}, {}, false, import("vue").ComponentOptionsBase<any, any, any, any, any, any, any, any, any, {}>> | null, info: string) => boolean | void)[]) | undefined;
};
$forceUpdate: () => void;
$nextTick: typeof import("vue").nextTick;
$watch(source: string | Function, cb: Function, options?: import("vue").WatchOptions<boolean> | undefined): import("vue").WatchStopHandle;
} & Readonly<import("vue").ExtractPropTypes<{
modelValue: {
type: import("vue").PropType<string>;
default: string;
};
theme: {
type: import("vue").PropType<import("./type").Themes>;
default: string;
};
class: {
type: StringConstructor;
default: string;
};
historyLength: {
type: import("vue").PropType<number>;
default: number;
};
onChange: {
type: import("vue").PropType<import("./type").ChangeEvent>;
};
onSave: {
type: import("vue").PropType<import("./type").SaveEvent>;
};
onUploadImg: {
type: import("vue").PropType<import("./type").UploadImgEvent>;
};
pageFullscreen: {
type: import("vue").PropType<boolean>;
default: boolean;
};
preview: {
type: import("vue").PropType<boolean>;
default: boolean;
};
htmlPreview: {
type: import("vue").PropType<boolean>;
default: boolean;
};
previewOnly: {
type: import("vue").PropType<boolean>;
default: boolean;
};
language: {
type: import("vue").PropType<string>;
default: string;
};
toolbars: {
type: import("vue").PropType<import("./type").ToolbarNames[]>;
default: string[];
};
toolbarsExclude: {
type: import("vue").PropType<import("./type").ToolbarNames[]>;
default: never[];
};
noPrettier: {
type: import("vue").PropType<boolean>;
default: boolean;
};
onHtmlChanged: {
type: import("vue").PropType<import("./type").HtmlChangedEvent>;
};
onGetCatalog: {
type: import("vue").PropType<import("./type").GetCatalogEvent>;
};
editorId: {
type: import("vue").PropType<string>;
default: string;
};
tabWidth: {
type: import("vue").PropType<number>;
default: number;
};
showCodeRowNumber: {
type: import("vue").PropType<boolean>;
default: boolean;
};
previewTheme: {
type: import("vue").PropType<string>;
default: string;
};
style: {
type: import("vue").PropType<string | import("vue").CSSProperties>;
default: () => {};
};
markedHeadingId: {
type: import("vue").PropType<import("./type").MarkedHeadingId>;
default: import("./type").MarkedHeadingId;
};
tableShape: {
type: import("vue").PropType<number[]>;
default: () => number[];
};
noMermaid: {
type: import("vue").PropType<boolean>;
default: boolean;
};
sanitize: {
type: import("vue").PropType<(html: string) => string>;
default: (html: string) => string;
};
placeholder: {
type: import("vue").PropType<string>;
default: string;
};
noKatex: {
type: import("vue").PropType<boolean>;
default: boolean;
};
defToolbars: {
type: import("vue").PropType<string | JSX.Element>;
};
onError: {
type: import("vue").PropType<import("./type").ErrorEvent>;
};
codeTheme: {
type: import("vue").PropType<string>;
default: string;
};
footers: {
type: import("vue").PropType<import("./type").Footers[]>;
default: import("./type").Footers[];
};
scrollAuto: {
type: import("vue").PropType<boolean>;
default: boolean;
};
defFooters: {
type: import("vue").PropType<string | JSX.Element>;
};
noIconfont: {
type: import("vue").PropType<boolean>;
};
formatCopiedText: {
type: import("vue").PropType<(text: string) => string>;
default: (text: string) => string;
};
noUploadImg: {
type: import("vue").PropType<boolean>;
};
codeStyleReverse: {
type: import("vue").PropType<boolean>;
default: boolean;
};
codeStyleReverseList: {
type: import("vue").PropType<string[]>;
default: string[];
};
autoFocus: {
type: import("vue").PropType<boolean>;
};
disabled: {
type: import("vue").PropType<boolean>;
};
readOnly: {
type: import("vue").PropType<boolean>;
};
maxLength: {
type: import("vue").PropType<number>;
};
autoDetectCode: {
type: import("vue").PropType<boolean>;
};
}>> & import("vue").ShallowUnwrapRef<() => JSX.Element> & {} & {} & import("vue").ComponentCustomProperties;
__isFragment?: undefined;
__isTeleport?: undefined;
__isSuspense?: undefined;
} & import("vue").ComponentOptionsBase<Readonly<import("vue").ExtractPropTypes<{
modelValue: {
type: import("vue").PropType<string>;
default: string;
};
theme: {
type: import("vue").PropType<import("./type").Themes>;
default: string;
};
class: {
type: StringConstructor;
default: string;
};
historyLength: {
type: import("vue").PropType<number>;
default: number;
};
onChange: {
type: import("vue").PropType<import("./type").ChangeEvent>;
};
onSave: {
type: import("vue").PropType<import("./type").SaveEvent>;
};
onUploadImg: {
type: import("vue").PropType<import("./type").UploadImgEvent>;
};
pageFullscreen: {
type: import("vue").PropType<boolean>;
default: boolean;
};
preview: {
type: import("vue").PropType<boolean>;
default: boolean;
};
htmlPreview: {
type: import("vue").PropType<boolean>;
default: boolean;
};
previewOnly: {
type: import("vue").PropType<boolean>;
default: boolean;
};
language: {
type: import("vue").PropType<string>;
default: string;
};
toolbars: {
type: import("vue").PropType<import("./type").ToolbarNames[]>;
default: string[];
};
toolbarsExclude: {
type: import("vue").PropType<import("./type").ToolbarNames[]>;
default: never[];
};
noPrettier: {
type: import("vue").PropType<boolean>;
default: boolean;
};
onHtmlChanged: {
type: import("vue").PropType<import("./type").HtmlChangedEvent>;
};
onGetCatalog: {
type: import("vue").PropType<import("./type").GetCatalogEvent>;
};
editorId: {
type: import("vue").PropType<string>;
default: string;
};
tabWidth: {
type: import("vue").PropType<number>;
default: number;
};
showCodeRowNumber: {
type: import("vue").PropType<boolean>;
default: boolean;
};
previewTheme: {
type: import("vue").PropType<string>;
default: string;
};
style: {
type: import("vue").PropType<string | import("vue").CSSProperties>;
default: () => {};
};
markedHeadingId: {
type: import("vue").PropType<import("./type").MarkedHeadingId>;
default: import("./type").MarkedHeadingId;
};
tableShape: {
type: import("vue").PropType<number[]>;
default: () => number[];
};
noMermaid: {
type: import("vue").PropType<boolean>;
default: boolean;
};
sanitize: {
type: import("vue").PropType<(html: string) => string>;
default: (html: string) => string;
};
placeholder: {
type: import("vue").PropType<string>;
default: string;
};
noKatex: {
type: import("vue").PropType<boolean>;
default: boolean;
};
defToolbars: {
type: import("vue").PropType<string | JSX.Element>;
};
onError: {
type: import("vue").PropType<import("./type").ErrorEvent>;
};
codeTheme: {
type: import("vue").PropType<string>;
default: string;
};
footers: {
type: import("vue").PropType<import("./type").Footers[]>;
default: import("./type").Footers[];
};
scrollAuto: {
type: import("vue").PropType<boolean>;
default: boolean;
};
defFooters: {
type: import("vue").PropType<string | JSX.Element>;
};
noIconfont: {
type: import("vue").PropType<boolean>;
};
formatCopiedText: {
type: import("vue").PropType<(text: string) => string>;
default: (text: string) => string;
};
noUploadImg: {
type: import("vue").PropType<boolean>;
};
codeStyleReverse: {
type: import("vue").PropType<boolean>;
default: boolean;
};
codeStyleReverseList: {
type: import("vue").PropType<string[]>;
default: string[];
};
autoFocus: {
type: import("vue").PropType<boolean>;
};
disabled: {
type: import("vue").PropType<boolean>;
};
readOnly: {
type: import("vue").PropType<boolean>;
};
maxLength: {
type: import("vue").PropType<number>;
};
autoDetectCode: {
type: import("vue").PropType<boolean>;
};
}>>, () => JSX.Element, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, import("vue").EmitsOptions, "onChange" | "onSave" | "onUploadImg" | "onHtmlChanged" | "onGetCatalog" | "onError" | "update:modelValue", {
pageFullscreen: boolean;
preview: boolean;
htmlPreview: boolean;
modelValue: string;
theme: import("./type").Themes;
class: string;
historyLength: number;
previewOnly: boolean;
language: string;
toolbars: import("./type").ToolbarNames[];
toolbarsExclude: import("./type").ToolbarNames[];
noPrettier: boolean;
editorId: string;
tabWidth: number;
showCodeRowNumber: boolean;
previewTheme: string;
style: string | import("vue").CSSProperties;
markedHeadingId: import("./type").MarkedHeadingId;
tableShape: number[];
noMermaid: boolean;
sanitize: (html: string) => string;
placeholder: string;
noKatex: boolean;
codeTheme: string;
footers: import("./type").Footers[];
scrollAuto: boolean;
formatCopiedText: (text: string) => string;
codeStyleReverse: boolean;
codeStyleReverseList: string[];
}> & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps & {
/**
* 默认工具栏组件
*/
readonly NormalToolbar: typeof NormalToolbar;
/**
* 下拉菜单工具栏组件
*/
readonly DropdownToolbar: typeof DropdownToolbar;
/**
* 目录组件
*/
readonly MdCatalog: typeof MdCatalog;
/**
* 弹窗工具栏组件
*/
readonly ModalToolbar: typeof ModalToolbar;
/**
* 配置编辑器全局内容
*/
readonly config: typeof config;
install: (app: App) => App;
};
export default _default;
export * from './type';

328
node_modules/md-editor-v3/lib/MdEditor/type.d.ts generated vendored

@ -0,0 +1,328 @@
import { marked, Renderer, Slugger } from 'marked';
declare global {
interface Window {
hljs: any;
prettier: any;
prettierPlugins: any;
Cropper: any;
screenfull: any;
mermaid: any;
katex: any;
}
}
export interface ToolbarTips {
bold?: string;
underline?: string;
italic?: string;
strikeThrough?: string;
title?: string;
sub?: string;
sup?: string;
quote?: string;
unorderedList?: string;
orderedList?: string;
task?: string;
codeRow?: string;
code?: string;
link?: string;
image?: string;
table?: string;
mermaid?: string;
katex?: string;
revoke?: string;
next?: string;
save?: string;
prettier?: string;
pageFullscreen?: string;
fullscreen?: string;
preview?: string;
htmlPreview?: string;
catalog?: string;
github?: string;
'-'?: string;
'='?: string;
}
export interface StaticTextDefaultValue {
toolbarTips?: ToolbarTips;
titleItem?: {
h1?: string;
h2?: string;
h3?: string;
h4?: string;
h5?: string;
h6?: string;
};
imgTitleItem?: {
link: string;
upload: string;
clip2upload: string;
};
linkModalTips?: {
linkTitle?: string;
imageTitle?: string;
descLabel?: string;
descLabelPlaceHolder?: string;
urlLabel?: string;
urlLabelPlaceHolder?: string;
buttonOK?: string;
};
clipModalTips?: {
title?: string;
buttonUpload?: string;
};
copyCode?: {
text?: string;
successTips?: string;
failTips?: string;
};
mermaid?: {
flow?: string;
sequence?: string;
gantt?: string;
class?: string;
state?: string;
pie?: string;
relationship?: string;
journey?: string;
};
katex?: {
inline: string;
block: string;
};
footer?: {
markdownTotal: string;
scrollAuto: string;
};
}
export interface StaticTextDefault {
'zh-CN': StaticTextDefaultValue;
'en-US': StaticTextDefaultValue;
}
export declare type StaticTextDefaultKey = keyof StaticTextDefault;
export declare type ToolbarNames = keyof ToolbarTips | number;
export declare type Footers = '=' | 'markdownTotal' | 'scrollSwitch' | number;
export interface SettingType {
pageFullscreen: boolean;
fullscreen: boolean;
preview: boolean;
htmlPreview: boolean;
}
export declare type Themes = 'light' | 'dark';
/**
* 预览主题
*
* @list ['default', 'github', 'vuepress', 'mk-cute', 'smart-blue', 'cyanosis']
*/
export declare type PreviewThemes = string;
export interface HeadList {
text: string;
level: 1 | 2 | 3 | 4 | 5 | 6;
active?: boolean;
}
export declare type MarkedHeadingId = (text: string, level: number, index: number) => string;
export interface MermaidTemplate {
/**
* 流程图
*/
flow?: string;
/**
* 时序图
*/
sequence?: string;
/**
* 甘特图
*/
gantt?: string;
/**
* 类图
*/
class?: string;
/**
* 状态图
*/
state?: string;
/**
* 饼图
*/
pie?: string;
/**
* 关系图
*/
relationship?: string;
/**
* 旅程图
*/
journey?: string;
}
export declare type RewriteHeading = (text: string, level: 1 | 2 | 3 | 4 | 5 | 6, raw: string, slugger: Slugger, index: number) => string;
export interface RewriteRenderer extends Omit<Renderer, 'heading'> {
heading: RewriteHeading;
}
export interface ConfigOption {
/**
* 覆盖编辑器默认的renderer属性
* @see https://marked.js.org/using_pro#renderer
*/
markedRenderer?: (renderer: RewriteRenderer) => RewriteRenderer;
/**
* 自定义 marked 扩展
* @see https://marked.js.org/using_pro#extensions
*/
markedExtensions?: Array<marked.TokenizerExtension & marked.RendererExtension>;
/**
* 自定义 marked option不推荐在这么覆盖renderer这会导致内部逻辑混乱
* @see https://marked.js.org/using_advanced#options
*/
markedOptions?: marked.MarkedOptions;
/**
* 编辑器内部依赖库
*/
editorExtensions?: {
highlight?: {
instance?: any;
js?: string;
css?: CodeCss;
};
prettier?: {
prettierInstance?: any;
parserMarkdownInstance?: any;
standaloneJs?: string;
parserMarkdownJs?: string;
};
cropper?: {
instance?: any;
js?: string;
css?: string;
};
iconfont?: string;
screenfull?: {
instance?: any;
js?: string;
};
mermaid?: {
instance?: any;
js?: string;
};
katex?: {
instance?: any;
js?: string;
css?: string;
};
};
editorConfig?: {
/**
* 自定义提示语言
*/
languageUserDefined?: {
[key: string]: StaticTextDefaultValue;
};
/**
* 自定义内部mermaid模块
*/
mermaidTemplate?: MermaidTemplate;
/**
* 输入渲染延迟ms
*/
renderDelay?: number;
};
}
/**
* 扩展编辑器内部功能包括marked和一些内部依赖实例如highlightcropper等
*/
export declare type Config = (options: ConfigOption) => void;
/**
* 编辑器操作潜在的错误
*/
export interface InnerError {
name: string;
message: string;
}
export interface CodeCss {
[key: string]: {
light: string;
dark: string;
};
}
export declare type UpdateSetting = (k: keyof SettingType, v?: boolean) => void;
export declare type ChangeEvent = (v: string) => void;
export declare type SaveEvent = (v: string, h: Promise<string>) => void;
export declare type UploadImgEvent = (files: Array<File>, callBack: (urls: string[]) => void) => void;
export declare type HtmlChangedEvent = (h: string) => void;
export declare type GetCatalogEvent = (list: HeadList[]) => void;
export declare type ErrorEvent = (err: InnerError) => void;
export interface ExposeEvent {
pageFullscreen(status: boolean): void;
fullscreen(status: boolean): void;
preview(status: boolean): void;
htmlPreview(status: boolean): void;
catalog(status: boolean): void;
}
export interface InsertParam {
targetValue: string;
select: boolean;
deviationStart: number;
deviationEnd: number;
}
export declare type InsertContentGenerator = (selectedText: string) => InsertParam;
export interface ExposeParam {
/**
* 添加事件监听
*
* @param eventName 事件名称
* @param callBack 事件回调函数
*/
on<E extends keyof ExposeEvent, C extends ExposeEvent[E]>(eventName: E, callBack: C): void;
/**
* 切换页面内全屏
*
* @param status 是否页面全屏
*/
togglePageFullscreen(status?: boolean): void;
/**
* 切换屏幕全屏
*
* @param status 是否屏幕全屏
*/
toggleFullscreen(status?: boolean): void;
/**
* 切换是否显示预览
*
* @param status 是否显示预览
*/
togglePreview(status?: boolean): void;
/**
* 切换是否显示html预览
*
* @param status html预览状态
*/
toggleHtmlPreview(status?: boolean): void;
/**
* 切换是否显示目录
*
* @param status 是否显示目录不设置默认相反
*/
toggleCatalog(status?: boolean): void;
/**
* 触发保存
*/
triggerSave(): void;
/**
* 手动向文本框插入内容
*
* @param {Function} generate 构造插入内容方法
* 构造方法提供当前选中的内容为入参
* 返回待插入内容和插入的属性
* 入参 selectedText 当前选中的内容
*
* targetValue 待插入内容
* select 插入后是否自动选中内容
* deviationStart 插入后选中位置的开始偏移量
* deviationEnd 插入后选中位置的结束偏移量
*
*/
insert(generate: InsertContentGenerator): void;
/**
* 手动聚焦
*/
focus(): void;
}

7079
node_modules/md-editor-v3/lib/md-editor-v3.es.js generated vendored

File diff suppressed because it is too large Load Diff

126
node_modules/md-editor-v3/lib/md-editor-v3.umd.js generated vendored

File diff suppressed because one or more lines are too long

1
node_modules/md-editor-v3/lib/style.css generated vendored

File diff suppressed because one or more lines are too long

124
node_modules/md-editor-v3/package.json generated vendored

@ -0,0 +1,124 @@
{
"name": "md-editor-v3",
"version": "2.7.2",
"keywords": [
"vue",
"vue3",
"javascript",
"typescript",
"jsx",
"tsx",
"markdown",
"editor",
"theme",
"html"
],
"description": "Markdown editor for vue3, developed by jsx and typescript, dark theme、beautify content by prettier、render articles directly、paste or clip the picture and upload it...",
"author": {
"name": "zbf",
"url": "https://imzbf.cc"
},
"homepage": "https://imzbf.github.io/md-editor-v3/",
"repository": {
"type": "git",
"url": "git+https://github.com/imzbf/md-editor-v3.git"
},
"bugs": {
"url": "https://github.com/imzbf/md-editor-v3/issues"
},
"files": [
"lib"
],
"type": "module",
"types": "./lib/MdEditor/index.d.ts",
"main": "./lib/md-editor-v3.umd.js",
"module": "./lib/md-editor-v3.es.js",
"exports": {
".": {
"import": "./lib/md-editor-v3.es.js",
"require": "./lib/md-editor-v3.umd.js"
},
"./lib/style.css": "./lib/style.css"
},
"license": "MIT",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"@babel/core": "^7.17.10",
"@babel/preset-env": "^7.17.10",
"@types/katex": "^0.14.0",
"@types/marked": "^4.0.7",
"@types/node": "16",
"@types/prettier": "^2.7.1",
"@typescript-eslint/eslint-plugin": "^5.23.0",
"@typescript-eslint/parser": "^5.23.0",
"@vavt/markdown-theme": "^1.1.0",
"@vitejs/plugin-vue": "^2.3.2",
"@vitejs/plugin-vue-jsx": "^1.3.10",
"@vue/compiler-sfc": "^3.2.33",
"axios": "^0.27.2",
"copy-to-clipboard": "^3.3.1",
"cropperjs": "^1.5.13",
"eslint": "^8.15.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"highlight.js": "^11.7.0",
"katex": "^0.16.3",
"less": "^4.1.1",
"marked": "^4.2.3",
"medium-zoom": "^1.0.8",
"mermaid": "^9.2.2",
"multiparty": "^4.2.2",
"prettier": "^2.8.0",
"screenfull": "5.2.0",
"ts-morph": "^14.0.0",
"typescript": "^4.6.4",
"vite": "^2.9.8",
"vite-plugin-dts": "^0.9.2",
"vue": "^3.2.33",
"vue-tsc": "^0.34.12"
},
"eslintConfig": {
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"prettier"
],
"parserOptions": {
"ecmaVersion": 12
},
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off"
}
},
"prettier": {
"singleQuote": true,
"trailingComma": "none",
"semi": true,
"printWidth": 90,
"proseWrap": "never",
"endOfLine": "auto"
},
"engines": {
"node": ">=12.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
Loading…
Cancel
Save