package handlers import ( "fmt" "log" "net/http" "os" "os/exec" "path/filepath" "strings" "github.com/gin-gonic/gin" ) // ExportHandler converts markdown content to PDF or DOCX via pandoc. type ExportHandler struct{} func NewExportHandler() *ExportHandler { return &ExportHandler{} } type exportRequest struct { Content string `json:"content" binding:"required"` Format string `json:"format" binding:"required"` // "pdf" or "docx" Filename string `json:"filename"` // optional base name } // Convert handles POST /api/v1/export. // Converts markdown content to the requested format and returns the file. func (h *ExportHandler) Convert(c *gin.Context) { var req exportRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Validate format switch req.Format { case "pdf", "docx": // ok default: c.JSON(http.StatusBadRequest, gin.H{"error": "format must be pdf or docx"}) return } // Check pandoc availability if _, err := exec.LookPath("pandoc"); err != nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "pandoc is not installed on the server"}) return } // Create temp dir for conversion tmpDir, err := os.MkdirTemp("", "export-*") if err != nil { log.Printf("export: failed to create temp dir: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "export failed"}) return } defer os.RemoveAll(tmpDir) // Write markdown to temp file inputPath := filepath.Join(tmpDir, "input.md") if err := os.WriteFile(inputPath, []byte(req.Content), 0644); err != nil { log.Printf("export: failed to write input: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "export failed"}) return } // Determine output filename baseName := "document" if req.Filename != "" { baseName = strings.TrimSuffix(req.Filename, filepath.Ext(req.Filename)) } outputFile := baseName + "." + req.Format outputPath := filepath.Join(tmpDir, outputFile) // Build pandoc command args := []string{inputPath, "-o", outputPath, "--standalone"} if req.Format == "pdf" { // Try to use a lightweight PDF engine for _, engine := range []string{"weasyprint", "wkhtmltopdf", "pdflatex"} { if _, err := exec.LookPath(engine); err == nil { args = append(args, "--pdf-engine="+engine) break } } } cmd := exec.CommandContext(c.Request.Context(), "pandoc", args...) cmd.Dir = tmpDir if output, err := cmd.CombinedOutput(); err != nil { log.Printf("export: pandoc failed: %v\n%s", err, string(output)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "pandoc conversion failed", "details": string(output), }) return } // Serve the file contentType := "application/octet-stream" switch req.Format { case "pdf": contentType = "application/pdf" case "docx": contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" } c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", outputFile)) c.File(outputPath) c.Header("Content-Type", contentType) }