新增:Markdown编辑器/数据库优化/安全修复
- Markdown 编辑器:实时预览、PDF 导出、独立查看器 - 数据库优化:动态连接池、查询缓存、Redis Pipeline - 窗口置顶功能 - 文件系统增强:右键菜单、编辑器集成、收藏夹重构 - 安全修复:XSS 防护、路径穿越、HTML 注入 - 代码质量:正则预编译、缓存锁优化、死代码清理
This commit is contained in:
@@ -1,18 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"u-desk/internal/storage"
|
||||
"u-desk/internal/service"
|
||||
"u-desk/internal/storage/models"
|
||||
)
|
||||
|
||||
// ConnectionAPI 连接管理API
|
||||
type ConnectionAPI struct {
|
||||
connService *storage.ConnectionService
|
||||
connService *service.ConnectionService
|
||||
}
|
||||
|
||||
// NewConnectionAPI 创建连接管理API
|
||||
func NewConnectionAPI() (*ConnectionAPI, error) {
|
||||
connService, err := storage.NewConnectionService()
|
||||
connService, err := service.NewConnectionService()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -82,11 +82,7 @@ func (api *ConnectionAPI) DeleteDbConnection(id uint) error {
|
||||
}
|
||||
|
||||
func (api *ConnectionAPI) TestDbConnection(id uint) error {
|
||||
conn, err := api.connService.GetConnection(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return api.connService.TestConnection(conn)
|
||||
return api.connService.TestConnection(id)
|
||||
}
|
||||
|
||||
// TestConnectionRequest 测试连接请求结构体(不保存数据)
|
||||
@@ -104,14 +100,9 @@ type TestConnectionRequest struct {
|
||||
// TestDbConnectionWithParams 测试数据库连接(直接传入参数,不保存数据)
|
||||
func (api *ConnectionAPI) TestDbConnectionWithParams(req TestConnectionRequest) error {
|
||||
return api.connService.TestConnectionWithParams(
|
||||
req.Type,
|
||||
req.Host,
|
||||
req.Port,
|
||||
req.Username,
|
||||
req.Password,
|
||||
req.Database,
|
||||
req.Options,
|
||||
req.ID,
|
||||
req.Type, req.Host, req.Port,
|
||||
req.Username, req.Password, req.Database,
|
||||
req.Options, req.ID,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -130,13 +121,8 @@ type LoadAllDatabasesRequest struct {
|
||||
// LoadAllDatabases 加载全部数据库列表
|
||||
func (api *ConnectionAPI) LoadAllDatabases(req LoadAllDatabasesRequest) ([]string, error) {
|
||||
return api.connService.LoadAllDatabases(
|
||||
req.Type,
|
||||
req.Host,
|
||||
req.Port,
|
||||
req.Username,
|
||||
req.Password,
|
||||
req.Database,
|
||||
req.Options,
|
||||
req.ID,
|
||||
req.Type, req.Host, req.Port,
|
||||
req.Username, req.Password, req.Database,
|
||||
req.Options, req.ID,
|
||||
)
|
||||
}
|
||||
|
||||
379
internal/api/pdf_api.go
Normal file
379
internal/api/pdf_api.go
Normal file
@@ -0,0 +1,379 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/page"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/yuin/goldmark"
|
||||
"u-desk/internal/common"
|
||||
)
|
||||
|
||||
// PdfExportRequest PDF导出请求结构体
|
||||
type PdfExportRequest struct {
|
||||
Content string `json:"content"` // Markdown/HTML内容
|
||||
Title string `json:"title"` // PDF标题
|
||||
FileName string `json:"fileName"` // 文件名(不含扩展名)
|
||||
FontSize int `json:"fontSize"` // 字体大小
|
||||
PageWidth int `json:"pageWidth"` // 页面宽度(mm)
|
||||
PageHeight int `json:"pageHeight"` // 页面高度(mm)
|
||||
}
|
||||
|
||||
// PdfExportResponse PDF导出响应结构体
|
||||
type PdfExportResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Path string `json:"path"` // PDF文件保存路径
|
||||
Size int64 `json:"size"` // 文件大小(字节)
|
||||
}
|
||||
|
||||
// PdfAPI PDF导出API
|
||||
type PdfAPI struct {
|
||||
// 可以在这里添加依赖,如文件系统服务等
|
||||
}
|
||||
|
||||
// NewPdfAPI 创建PDF导出API
|
||||
func NewPdfAPI() (*PdfAPI, error) {
|
||||
return &PdfAPI{}, nil
|
||||
}
|
||||
|
||||
// ExportMarkdownToPDF 将Markdown内容导出为PDF - 使用chromedp实现
|
||||
func (api *PdfAPI) ExportMarkdownToPDF(req PdfExportRequest) (*PdfExportResponse, error) {
|
||||
// 验证参数
|
||||
if strings.TrimSpace(req.Content) == "" {
|
||||
return nil, fmt.Errorf("内容不能为空")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.FileName) == "" {
|
||||
req.FileName = "document_" + time.Now().Format("20060102_150405")
|
||||
}
|
||||
|
||||
if req.FontSize <= 0 {
|
||||
req.FontSize = 12
|
||||
}
|
||||
|
||||
// 设置默认页面尺寸(A4)
|
||||
if req.PageWidth <= 0 {
|
||||
req.PageWidth = 210
|
||||
}
|
||||
if req.PageHeight <= 0 {
|
||||
req.PageHeight = 297
|
||||
}
|
||||
|
||||
// 将Markdown转换为HTML
|
||||
htmlContent := api.markdownToHTML(req.Content, req.Title, req.FontSize)
|
||||
|
||||
// 使用chromedp生成PDF
|
||||
pdfBuffer, err := api.generatePDFFromHTML(htmlContent, req.Title, req.PageWidth, req.PageHeight)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("生成PDF失败: %v", err)
|
||||
}
|
||||
|
||||
// 生成文件名
|
||||
if !strings.HasSuffix(strings.ToLower(req.FileName), ".pdf") {
|
||||
req.FileName += ".pdf"
|
||||
}
|
||||
|
||||
// 获取用户桌面目录作为默认保存位置
|
||||
saveDir := api.getDesktopDirectory()
|
||||
|
||||
// 确保目录存在
|
||||
if err := os.MkdirAll(saveDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("创建目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 完整保存路径
|
||||
savePath := filepath.Join(saveDir, filepath.Base(req.FileName))
|
||||
|
||||
// 保存PDF文件
|
||||
err = os.WriteFile(savePath, pdfBuffer, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("保存PDF文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取文件信息
|
||||
fileInfo, err := os.Stat(savePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取文件信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 返回成功响应
|
||||
return &PdfExportResponse{
|
||||
Success: true,
|
||||
Message: "PDF生成成功",
|
||||
Path: savePath,
|
||||
Size: fileInfo.Size(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// markdownToHTML 将Markdown转换为HTML
|
||||
func (api *PdfAPI) markdownToHTML(markdownContent string, title string, fontSize int) string {
|
||||
// 基础HTML模板
|
||||
htmlTemplate := `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
font-size: %dpx;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid #eaecef;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
h5 {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
h6 {
|
||||
font-size: 0.85em;
|
||||
color: #6a737d;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
blockquote {
|
||||
margin: 0 0 16px;
|
||||
padding: 0 1em;
|
||||
color: #6a737d;
|
||||
border-left: 0.25em solid #dfe2e5;
|
||||
}
|
||||
ul, ol {
|
||||
padding-left: 2em;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
code {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
background-color: rgba(27,31,35,0.05);
|
||||
border-radius: 3px;
|
||||
font-size: 85%;
|
||||
margin: 0;
|
||||
padding: 0.2em 0.4em;
|
||||
}
|
||||
pre {
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 3px;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #dfe2e5;
|
||||
}
|
||||
th, td {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #dfe2e5;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #f6f8fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 16px 0;
|
||||
}
|
||||
hr {
|
||||
height: 0.25em;
|
||||
padding: 0;
|
||||
margin: 24px 0;
|
||||
background-color: #e1e4e8;
|
||||
border: 0;
|
||||
}
|
||||
.title {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
font-size: 1.5em;
|
||||
color: #2c3e50;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="title">%s</div>
|
||||
%s
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// 标题处理
|
||||
docTitle := ""
|
||||
if title != "" {
|
||||
docTitle = html.EscapeString(title)
|
||||
} else {
|
||||
docTitle = "文档"
|
||||
}
|
||||
|
||||
// Markdown转HTML(使用goldmark)
|
||||
var htmlContent string
|
||||
var htmlBuf strings.Builder
|
||||
if err := goldmark.Convert([]byte(markdownContent), &htmlBuf); err != nil {
|
||||
htmlContent = "<p>Markdown 解析失败</p>"
|
||||
} else {
|
||||
htmlContent = htmlBuf.String()
|
||||
}
|
||||
|
||||
// 生成完整的HTML
|
||||
fullHTML := fmt.Sprintf(htmlTemplate, fontSize, docTitle, htmlContent)
|
||||
|
||||
return fullHTML
|
||||
}
|
||||
|
||||
// generatePDFFromHTML 使用chromedp从HTML生成PDF
|
||||
func (api *PdfAPI) generatePDFFromHTML(htmlContent, title string, pageWidth, pageHeight int) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
// 配置chromedp选项
|
||||
opts := []chromedp.ExecAllocatorOption{
|
||||
chromedp.Flag("headless", true),
|
||||
chromedp.Flag("disable-gpu", true),
|
||||
chromedp.Flag("no-sandbox", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
chromedp.Flag("disable-software-rasterizer", true),
|
||||
chromedp.Flag("disable-extensions", true),
|
||||
chromedp.Flag("disable-notifications", true),
|
||||
}
|
||||
|
||||
// 在Windows上设置Chrome路径
|
||||
if common.IsWindows() {
|
||||
// 常见的Windows Chrome路径
|
||||
chromePaths := []string{
|
||||
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
||||
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
||||
"C:\\Users\\" + os.Getenv("USERNAME") + "\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe",
|
||||
}
|
||||
|
||||
for _, path := range chromePaths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
opts = append(opts, chromedp.ExecPath(path))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建执行分配器上下文
|
||||
allocCtx, allocCancel := chromedp.NewExecAllocator(ctx, opts...)
|
||||
defer allocCancel()
|
||||
|
||||
// 创建chromedp上下文
|
||||
chromeCtx, chromeCancel := chromedp.NewContext(allocCtx)
|
||||
defer chromeCancel()
|
||||
|
||||
// 创建一个临时的目录用于PDF生成
|
||||
tempDir, err := os.MkdirTemp("", "pdf_gen")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建临时目录失败: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// 将HTML写入临时文件
|
||||
htmlFile := filepath.Join(tempDir, "document.html")
|
||||
if err := os.WriteFile(htmlFile, []byte(htmlContent), 0644); err != nil {
|
||||
return nil, fmt.Errorf("写入HTML文件失败: %v", err)
|
||||
}
|
||||
|
||||
var buf []byte
|
||||
|
||||
// 使用 file URL 加载本地HTML文件
|
||||
err = chromedp.Run(chromeCtx,
|
||||
// 导航到HTML文件
|
||||
chromedp.Navigate("file://"+htmlFile),
|
||||
// 等待页面加载完成
|
||||
chromedp.WaitReady("body"),
|
||||
// 打印到PDF
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
// 设置页面打印参数
|
||||
printToPDF := page.PrintToPDF().
|
||||
WithPrintBackground(true).
|
||||
WithLandscape(false).
|
||||
WithMarginTop(0).
|
||||
WithMarginBottom(0).
|
||||
WithMarginLeft(0).
|
||||
WithMarginRight(0).
|
||||
WithPaperWidth(float64(pageWidth) / 25.4). // mm to inches
|
||||
WithPaperHeight(float64(pageHeight) / 25.4) // mm to inches
|
||||
|
||||
// 执行打印并获取PDF数据
|
||||
var err error
|
||||
buf, _, err = printToPDF.Do(ctx)
|
||||
return err
|
||||
}),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("chromedp执行失败: %v", err)
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// getDesktopDirectory 获取用户桌面目录
|
||||
func (api *PdfAPI) getDesktopDirectory() string {
|
||||
// Windows系统
|
||||
if common.IsWindows() {
|
||||
home := os.Getenv("USERPROFILE")
|
||||
if home != "" {
|
||||
return filepath.Join(home, "Desktop")
|
||||
}
|
||||
}
|
||||
|
||||
// Linux/Mac系统
|
||||
home := os.Getenv("HOME")
|
||||
if home != "" {
|
||||
return filepath.Join(home, "Desktop")
|
||||
}
|
||||
|
||||
// 备用:当前目录
|
||||
return "."
|
||||
}
|
||||
|
||||
// SelectDirectory 选择保存目录(简化版,实际应该使用Wails runtime)
|
||||
func (api *PdfAPI) SelectDirectory() (string, error) {
|
||||
// 简化版:直接返回桌面目录
|
||||
desktop := api.getDesktopDirectory()
|
||||
if desktop == "." {
|
||||
return "", fmt.Errorf("无法确定默认目录")
|
||||
}
|
||||
return desktop, nil
|
||||
}
|
||||
Reference in New Issue
Block a user