PDF generation and downloads are working
This commit is contained in:
+2
-1
@@ -42,4 +42,5 @@ bamort
|
|||||||
maintenance/testdata/*
|
maintenance/testdata/*
|
||||||
testdata/*_data.db
|
testdata/*_data.db
|
||||||
tmp/main
|
tmp/main
|
||||||
uploads/*
|
uploads/*
|
||||||
|
xporttemp/*
|
||||||
@@ -68,6 +68,10 @@ func main() {
|
|||||||
maintenance.RegisterRoutes(protected)
|
maintenance.RegisterRoutes(protected)
|
||||||
importer.RegisterRoutes(protected)
|
importer.RegisterRoutes(protected)
|
||||||
pdfrender.RegisterRoutes(protected)
|
pdfrender.RegisterRoutes(protected)
|
||||||
|
|
||||||
|
// Register public routes (no authentication)
|
||||||
|
pdfrender.RegisterPublicRoutes(r)
|
||||||
|
|
||||||
logger.Info("API-Routen erfolgreich registriert")
|
logger.Info("API-Routen erfolgreich registriert")
|
||||||
|
|
||||||
// Server starten
|
// Server starten
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ type Config struct {
|
|||||||
DevTesting string // "yes" or "no", used to determine if we are in a test environment
|
DevTesting string // "yes" or "no", used to determine if we are in a test environment
|
||||||
|
|
||||||
// PDF Templates
|
// PDF Templates
|
||||||
TemplatesDir string // Directory where PDF templates are stored
|
TemplatesDir string // Directory where PDF templates are stored
|
||||||
|
ExportTempDir string // Directory for temporary PDF exports
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cfg ist die globale Konfigurationsvariable
|
// Cfg ist die globale Konfigurationsvariable
|
||||||
@@ -42,14 +43,15 @@ func init() {
|
|||||||
// defaultConfig gibt die Standard-Konfiguration zurück
|
// defaultConfig gibt die Standard-Konfiguration zurück
|
||||||
func defaultConfig() *Config {
|
func defaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
ServerPort: "8180",
|
ServerPort: "8180",
|
||||||
DatabaseURL: "",
|
DatabaseURL: "",
|
||||||
DatabaseType: "mysql",
|
DatabaseType: "mysql",
|
||||||
DebugMode: false,
|
DebugMode: false,
|
||||||
LogLevel: "INFO",
|
LogLevel: "INFO",
|
||||||
Environment: "production",
|
Environment: "production",
|
||||||
DevTesting: "no", // Default to "no", can be overridden in tests
|
DevTesting: "no", // Default to "no", can be overridden in tests
|
||||||
TemplatesDir: "./templates", // Default templates directory
|
TemplatesDir: "./templates", // Default templates directory
|
||||||
|
ExportTempDir: "./xporttemp", // Default export temp directory
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +122,11 @@ func LoadConfig() *Config {
|
|||||||
config.TemplatesDir = templatesDir
|
config.TemplatesDir = templatesDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export Temp Directory
|
||||||
|
if exportTempDir := os.Getenv("EXPORT_TEMP_DIR"); exportTempDir != "" {
|
||||||
|
config.ExportTempDir = exportTempDir
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("DEBUG LoadConfig - Finale Config: Environment='%s', DevTesting='%s', DatabaseType='%s'\n",
|
fmt.Printf("DEBUG LoadConfig - Finale Config: Environment='%s', DevTesting='%s', DatabaseType='%s'\n",
|
||||||
config.Environment, config.DevTesting, config.DatabaseType)
|
config.Environment, config.DevTesting, config.DatabaseType)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package pdfrender
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SanitizeFilename removes or replaces characters that are problematic in filenames
|
||||||
|
func SanitizeFilename(name string) string {
|
||||||
|
// Replace umlauts
|
||||||
|
replacements := map[string]string{
|
||||||
|
"ä": "ae", "Ä": "Ae",
|
||||||
|
"ö": "oe", "Ö": "Oe",
|
||||||
|
"ü": "ue", "Ü": "Ue",
|
||||||
|
"ß": "ss",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := name
|
||||||
|
for old, new := range replacements {
|
||||||
|
result = strings.ReplaceAll(result, old, new)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace problematic characters with underscore
|
||||||
|
reg := regexp.MustCompile(`[^a-zA-Z0-9_.-]+`)
|
||||||
|
result = reg.ReplaceAllString(result, "_")
|
||||||
|
|
||||||
|
// Remove consecutive underscores
|
||||||
|
reg = regexp.MustCompile(`_+`)
|
||||||
|
result = reg.ReplaceAllString(result, "_")
|
||||||
|
|
||||||
|
// Trim underscores from start and end
|
||||||
|
result = strings.Trim(result, "_")
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateExportFilename creates a filename from character name and timestamp
|
||||||
|
func GenerateExportFilename(charName string, timestamp time.Time) string {
|
||||||
|
sanitizedName := SanitizeFilename(charName)
|
||||||
|
timeStr := timestamp.Format("20060102_150405")
|
||||||
|
return sanitizedName + "_" + timeStr + ".pdf"
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureExportTempDir creates the export temp directory if it doesn't exist
|
||||||
|
func EnsureExportTempDir(dir string) error {
|
||||||
|
return os.MkdirAll(dir, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupOldFiles removes files older than maxAge from the directory
|
||||||
|
// Returns the number of files deleted
|
||||||
|
func CleanupOldFiles(dir string, maxAge time.Duration) (int, error) {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
cutoff := time.Now().Add(-maxAge)
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(dir, entry.Name())
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.ModTime().Before(cutoff) {
|
||||||
|
if err := os.Remove(filePath); err == nil {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package pdfrender
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTestEnvironment(t *testing.T) {
|
||||||
|
original := os.Getenv("ENVIRONMENT")
|
||||||
|
os.Setenv("ENVIRONMENT", "test")
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if original != "" {
|
||||||
|
os.Setenv("ENVIRONMENT", original)
|
||||||
|
} else {
|
||||||
|
os.Unsetenv("ENVIRONMENT")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSanitizeFilename(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Simple name",
|
||||||
|
input: "Character",
|
||||||
|
expected: "Character",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Name with spaces",
|
||||||
|
input: "Fanjo Vetrani",
|
||||||
|
expected: "Fanjo_Vetrani",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Name with umlauts",
|
||||||
|
input: "Müller Ökonom",
|
||||||
|
expected: "Mueller_Oekonom",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Name with special chars",
|
||||||
|
input: "Test/Character\\Name:With*Special?Chars",
|
||||||
|
expected: "Test_Character_Name_With_Special_Chars",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple consecutive spaces",
|
||||||
|
input: "Test Name",
|
||||||
|
expected: "Test_Name",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := SanitizeFilename(tt.input)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("SanitizeFilename(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateExportFilename(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
|
||||||
|
// Test with a fixed timestamp
|
||||||
|
timestamp := time.Date(2023, 12, 25, 14, 30, 45, 0, time.UTC)
|
||||||
|
filename := GenerateExportFilename("Fanjo Vetrani", timestamp)
|
||||||
|
|
||||||
|
expectedPrefix := "Fanjo_Vetrani_20231225_143045"
|
||||||
|
if filename != expectedPrefix+".pdf" {
|
||||||
|
t.Errorf("GenerateExportFilename() = %q, want prefix %q", filename, expectedPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with special characters
|
||||||
|
filename2 := GenerateExportFilename("Test/Char*Name", timestamp)
|
||||||
|
expectedPrefix2 := "Test_Char_Name_20231225_143045"
|
||||||
|
if filename2 != expectedPrefix2+".pdf" {
|
||||||
|
t.Errorf("GenerateExportFilename() = %q, want prefix %q", filename2, expectedPrefix2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnsureExportTempDir(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
|
||||||
|
// Use a test-specific temp directory
|
||||||
|
testDir := filepath.Join(os.TempDir(), "bamort_test_xporttemp")
|
||||||
|
defer os.RemoveAll(testDir)
|
||||||
|
|
||||||
|
// First call should create the directory
|
||||||
|
err := EnsureExportTempDir(testDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EnsureExportTempDir() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify directory exists
|
||||||
|
info, err := os.Stat(testDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Directory not created: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.IsDir() {
|
||||||
|
t.Error("Path exists but is not a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second call should succeed (directory already exists)
|
||||||
|
err = EnsureExportTempDir(testDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("EnsureExportTempDir() on existing directory error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanupOldFiles(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
|
||||||
|
// Create test directory
|
||||||
|
testDir := filepath.Join(os.TempDir(), "bamort_test_cleanup")
|
||||||
|
os.RemoveAll(testDir)
|
||||||
|
defer os.RemoveAll(testDir)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create test directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create test files with different ages
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Old file (8 days old)
|
||||||
|
oldFile := filepath.Join(testDir, "old_file.pdf")
|
||||||
|
if err := os.WriteFile(oldFile, []byte("old"), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create old file: %v", err)
|
||||||
|
}
|
||||||
|
oldTime := now.Add(-8 * 24 * time.Hour)
|
||||||
|
if err := os.Chtimes(oldFile, oldTime, oldTime); err != nil {
|
||||||
|
t.Fatalf("Failed to set old file time: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recent file (3 days old)
|
||||||
|
recentFile := filepath.Join(testDir, "recent_file.pdf")
|
||||||
|
if err := os.WriteFile(recentFile, []byte("recent"), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create recent file: %v", err)
|
||||||
|
}
|
||||||
|
recentTime := now.Add(-3 * 24 * time.Hour)
|
||||||
|
if err := os.Chtimes(recentFile, recentTime, recentTime); err != nil {
|
||||||
|
t.Fatalf("Failed to set recent file time: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New file (1 hour old)
|
||||||
|
newFile := filepath.Join(testDir, "new_file.pdf")
|
||||||
|
if err := os.WriteFile(newFile, []byte("new"), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create new file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup files older than 7 days
|
||||||
|
count, err := CleanupOldFiles(testDir, 7*24*time.Hour)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CleanupOldFiles() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if count != 1 {
|
||||||
|
t.Errorf("CleanupOldFiles() deleted %d files, want 1", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify old file is deleted
|
||||||
|
if _, err := os.Stat(oldFile); !os.IsNotExist(err) {
|
||||||
|
t.Error("Old file should be deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify recent file still exists
|
||||||
|
if _, err := os.Stat(recentFile); err != nil {
|
||||||
|
t.Error("Recent file should still exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify new file still exists
|
||||||
|
if _, err := os.Stat(newFile); err != nil {
|
||||||
|
t.Error("New file should still exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,10 +45,12 @@ func ListTemplates(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, templates)
|
c.JSON(http.StatusOK, templates)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportCharacterToPDF exports a character to PDF
|
// ExportCharacterToPDF exports a character to PDF and saves it to xporttemp directory
|
||||||
// Query params:
|
// Query params:
|
||||||
// - template: template ID to use (default: "Default_A4_Quer")
|
// - template: template ID to use (default: "Default_A4_Quer")
|
||||||
// - showUserName: whether to show user name (default: false)
|
// - showUserName: whether to show user name (default: false)
|
||||||
|
//
|
||||||
|
// Returns JSON with filename: {"filename": "CharacterName_20231225_143045.pdf"}
|
||||||
func ExportCharacterToPDF(c *gin.Context) {
|
func ExportCharacterToPDF(c *gin.Context) {
|
||||||
// Get character ID
|
// Get character ID
|
||||||
charID := c.Param("id")
|
charID := c.Param("id")
|
||||||
@@ -120,47 +122,110 @@ func ExportCharacterToPDF(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
allPDFs = append(allPDFs, page4PDFs...)
|
allPDFs = append(allPDFs, page4PDFs...)
|
||||||
|
|
||||||
// If only one PDF, return it directly
|
// Merge PDFs if needed
|
||||||
|
var finalPDF []byte
|
||||||
if len(allPDFs) == 1 {
|
if len(allPDFs) == 1 {
|
||||||
c.Data(http.StatusOK, "application/pdf", allPDFs[0])
|
finalPDF = allPDFs[0]
|
||||||
return
|
} else {
|
||||||
}
|
// Merge multiple PDFs
|
||||||
|
tmpDir := "/tmp/bamort_pdf_export"
|
||||||
// Merge multiple PDFs
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
tmpDir := "/tmp/bamort_pdf_export"
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create temp directory"})
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
return
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create temp directory"})
|
}
|
||||||
return
|
defer os.RemoveAll(tmpDir)
|
||||||
}
|
|
||||||
defer os.RemoveAll(tmpDir)
|
// Save individual PDFs
|
||||||
|
var filePaths []string
|
||||||
// Save individual PDFs
|
for i, pdf := range allPDFs {
|
||||||
var filePaths []string
|
filename := fmt.Sprintf("%s/page_%d.pdf", tmpDir, i)
|
||||||
for i, pdf := range allPDFs {
|
if err := os.WriteFile(filename, pdf, 0644); err != nil {
|
||||||
filename := fmt.Sprintf("%s/page_%d.pdf", tmpDir, i)
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to write temporary PDF"})
|
||||||
if err := os.WriteFile(filename, pdf, 0644); err != nil {
|
return
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to write temporary PDF"})
|
}
|
||||||
|
filePaths = append(filePaths, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge PDFs
|
||||||
|
combinedPath := fmt.Sprintf("%s/combined.pdf", tmpDir)
|
||||||
|
if err := api.MergeCreateFile(filePaths, combinedPath, false, nil); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to merge PDFs: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read combined PDF
|
||||||
|
finalPDF, err = os.ReadFile(combinedPath)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read combined PDF"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
filePaths = append(filePaths, filename)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge PDFs
|
// Ensure export temp directory exists
|
||||||
combinedPath := fmt.Sprintf("%s/combined.pdf", tmpDir)
|
if err := EnsureExportTempDir(config.Cfg.ExportTempDir); err != nil {
|
||||||
if err := api.MergeCreateFile(filePaths, combinedPath, false, nil); err != nil {
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create export directory"})
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to merge PDFs: " + err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read combined PDF
|
// Generate filename
|
||||||
combinedPDF, err := os.ReadFile(combinedPath)
|
filename := GenerateExportFilename(char.Name, time.Now())
|
||||||
if err != nil {
|
filePath := filepath.Join(config.Cfg.ExportTempDir, filename)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read combined PDF"})
|
|
||||||
|
// Save PDF to file
|
||||||
|
if err := os.WriteFile(filePath, finalPDF, 0644); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save PDF file: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set response headers
|
// Return filename
|
||||||
filename := fmt.Sprintf("%s_character_sheet.pdf", char.Name)
|
c.JSON(http.StatusOK, gin.H{"filename": filename})
|
||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
}
|
||||||
c.Data(http.StatusOK, "application/pdf", combinedPDF)
|
|
||||||
|
// GetPDFFile serves a PDF file from the xporttemp directory
|
||||||
|
func GetPDFFile(c *gin.Context) {
|
||||||
|
filename := c.Param("filename")
|
||||||
|
if filename == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Filename is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent path traversal attacks - only allow base filename
|
||||||
|
if filepath.Base(filename) != filename {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid filename"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow .pdf files
|
||||||
|
if filepath.Ext(filename) != ".pdf" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Only PDF files are allowed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct full path
|
||||||
|
filePath := filepath.Join(config.Cfg.ExportTempDir, filename)
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "File not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve the file
|
||||||
|
c.File(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanupExportTemp removes PDF files older than 7 days from xporttemp directory
|
||||||
|
func CleanupExportTemp(c *gin.Context) {
|
||||||
|
// Clean up files older than 7 days
|
||||||
|
maxAge := 7 * 24 * time.Hour
|
||||||
|
count, err := CleanupOldFiles(config.Cfg.ExportTempDir, maxAge)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cleanup files: " + err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"deleted": count,
|
||||||
|
"message": fmt.Sprintf("Deleted %d files older than 7 days", count),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -15,6 +19,7 @@ import (
|
|||||||
func init() {
|
func init() {
|
||||||
// Set templates directory for tests (tests run from pdfrender/ directory)
|
// Set templates directory for tests (tests run from pdfrender/ directory)
|
||||||
config.Cfg.TemplatesDir = "../templates"
|
config.Cfg.TemplatesDir = "../templates"
|
||||||
|
config.Cfg.ExportTempDir = "../xporttemp"
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListTemplates(t *testing.T) {
|
func TestListTemplates(t *testing.T) {
|
||||||
@@ -59,6 +64,14 @@ func TestExportCharacterToPDF(t *testing.T) {
|
|||||||
// Arrange
|
// Arrange
|
||||||
database.SetupTestDB()
|
database.SetupTestDB()
|
||||||
|
|
||||||
|
// Use test-specific temp directory
|
||||||
|
testDir := "../xporttemp_test_basic"
|
||||||
|
config.Cfg.ExportTempDir = testDir
|
||||||
|
defer func() {
|
||||||
|
os.RemoveAll(testDir)
|
||||||
|
config.Cfg.ExportTempDir = "../xporttemp"
|
||||||
|
}()
|
||||||
|
|
||||||
// Load test character
|
// Load test character
|
||||||
char := &models.Char{}
|
char := &models.Char{}
|
||||||
err := char.FirstID("18")
|
err := char.FirstID("18")
|
||||||
@@ -77,24 +90,41 @@ func TestExportCharacterToPDF(t *testing.T) {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
if w.Code != http.StatusOK {
|
if w.Code != http.StatusOK {
|
||||||
t.Errorf("Expected status 200, got %d", w.Code)
|
t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check content type
|
// Check content type - now returns JSON
|
||||||
contentType := w.Header().Get("Content-Type")
|
contentType := w.Header().Get("Content-Type")
|
||||||
if contentType != "application/pdf" {
|
if !strings.Contains(contentType, "application/json") {
|
||||||
t.Errorf("Expected Content-Type 'application/pdf', got '%s'", contentType)
|
t.Errorf("Expected Content-Type 'application/json', got '%s'", contentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check PDF content
|
// Parse JSON response
|
||||||
body := w.Body.Bytes()
|
var response map[string]string
|
||||||
if len(body) == 0 {
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||||
t.Error("Expected non-empty PDF content")
|
t.Fatalf("Failed to parse JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify filename is returned
|
||||||
|
filename, ok := response["filename"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Response should contain 'filename' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
if filename == "" {
|
||||||
|
t.Error("Filename should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify PDF file exists
|
||||||
|
filePath := filepath.Join(testDir, filename)
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read PDF file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify PDF marker
|
// Verify PDF marker
|
||||||
if string(body[0:4]) != "%PDF" {
|
if string(data[0:4]) != "%PDF" {
|
||||||
t.Error("Response does not start with PDF marker")
|
t.Error("File does not start with PDF marker")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +132,14 @@ func TestExportCharacterToPDF_WithTemplate(t *testing.T) {
|
|||||||
// Arrange
|
// Arrange
|
||||||
database.SetupTestDB()
|
database.SetupTestDB()
|
||||||
|
|
||||||
|
// Use test-specific temp directory
|
||||||
|
testDir := "../xporttemp_test_template"
|
||||||
|
config.Cfg.ExportTempDir = testDir
|
||||||
|
defer func() {
|
||||||
|
os.RemoveAll(testDir)
|
||||||
|
config.Cfg.ExportTempDir = "../xporttemp"
|
||||||
|
}()
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.GET("/export/:id", ExportCharacterToPDF)
|
router.GET("/export/:id", ExportCharacterToPDF)
|
||||||
@@ -113,13 +151,26 @@ func TestExportCharacterToPDF_WithTemplate(t *testing.T) {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
if w.Code != http.StatusOK {
|
if w.Code != http.StatusOK {
|
||||||
t.Errorf("Expected status 200, got %d", w.Code)
|
t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse JSON response
|
||||||
|
var response map[string]string
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||||
|
t.Fatalf("Failed to parse JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := response["filename"]
|
||||||
|
filePath := filepath.Join(testDir, filename)
|
||||||
|
|
||||||
// Verify it's a PDF
|
// Verify it's a PDF
|
||||||
body := w.Body.Bytes()
|
data, err := os.ReadFile(filePath)
|
||||||
if string(body[0:4]) != "%PDF" {
|
if err != nil {
|
||||||
t.Error("Response does not start with PDF marker")
|
t.Fatalf("Failed to read PDF: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(data[0:4]) != "%PDF" {
|
||||||
|
t.Error("File does not start with PDF marker")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,3 +192,270 @@ func TestExportCharacterToPDF_CharacterNotFound(t *testing.T) {
|
|||||||
t.Errorf("Expected status 404, got %d", w.Code)
|
t.Errorf("Expected status 404, got %d", w.Code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExportCharacterToPDF_SavesFileAndReturnsFilename(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
database.SetupTestDB()
|
||||||
|
|
||||||
|
// Use test-specific temp directory
|
||||||
|
testDir := "../xporttemp_test"
|
||||||
|
config.Cfg.ExportTempDir = testDir
|
||||||
|
defer func() {
|
||||||
|
// Cleanup
|
||||||
|
os.RemoveAll(testDir)
|
||||||
|
config.Cfg.ExportTempDir = "../xporttemp"
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Load test character
|
||||||
|
char := &models.Char{}
|
||||||
|
err := char.FirstID("18")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to load test character: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.New()
|
||||||
|
router.GET("/export/:id", ExportCharacterToPDF)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
req, _ := http.NewRequest("GET", "/export/18", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
var response map[string]string
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||||
|
t.Fatalf("Failed to parse JSON response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that filename is returned
|
||||||
|
filename, ok := response["filename"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Response should contain 'filename' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
if filename == "" {
|
||||||
|
t.Error("Filename should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify filename format (should contain character name and timestamp)
|
||||||
|
if !strings.Contains(filename, "Fanjo_Vetrani") {
|
||||||
|
t.Errorf("Filename should contain sanitized character name, got: %s", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(filename, ".pdf") {
|
||||||
|
t.Errorf("Filename should end with .pdf, got: %s", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify file exists in xporttemp directory
|
||||||
|
filePath := filepath.Join(testDir, filename)
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
t.Errorf("PDF file should exist at %s", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify it's a valid PDF
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to read PDF file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
t.Error("PDF file should not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(data[0:4]) != "%PDF" {
|
||||||
|
t.Error("File should be a valid PDF")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPDFFile(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
|
||||||
|
// Create test directory and file
|
||||||
|
testDir := "../xporttemp_test_get"
|
||||||
|
config.Cfg.ExportTempDir = testDir
|
||||||
|
defer func() {
|
||||||
|
os.RemoveAll(testDir)
|
||||||
|
config.Cfg.ExportTempDir = "../xporttemp"
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create test directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a test PDF file
|
||||||
|
testFilename := "Test_Character_20231225_120000.pdf"
|
||||||
|
testPDFContent := []byte("%PDF-1.4\nTest PDF Content")
|
||||||
|
filePath := filepath.Join(testDir, testFilename)
|
||||||
|
if err := os.WriteFile(filePath, testPDFContent, 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.New()
|
||||||
|
router.GET("/file/:filename", GetPDFFile)
|
||||||
|
|
||||||
|
// Act - Get the PDF file
|
||||||
|
req, _ := http.NewRequest("GET", "/file/"+testFilename, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check content type
|
||||||
|
contentType := w.Header().Get("Content-Type")
|
||||||
|
if contentType != "application/pdf" {
|
||||||
|
t.Errorf("Expected Content-Type 'application/pdf', got '%s'", contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check content
|
||||||
|
body := w.Body.Bytes()
|
||||||
|
if string(body) != string(testPDFContent) {
|
||||||
|
t.Errorf("Expected PDF content, got different content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPDFFile_NotFound(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
|
||||||
|
testDir := "../xporttemp_test_notfound"
|
||||||
|
config.Cfg.ExportTempDir = testDir
|
||||||
|
defer func() {
|
||||||
|
os.RemoveAll(testDir)
|
||||||
|
config.Cfg.ExportTempDir = "../xporttemp"
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create test directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.New()
|
||||||
|
router.GET("/file/:filename", GetPDFFile)
|
||||||
|
|
||||||
|
// Act - Try to get non-existent file
|
||||||
|
req, _ := http.NewRequest("GET", "/file/nonexistent.pdf", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Errorf("Expected status 404, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPDFFile_PathTraversal(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
|
||||||
|
testDir := "../xporttemp_test_security"
|
||||||
|
config.Cfg.ExportTempDir = testDir
|
||||||
|
defer func() {
|
||||||
|
os.RemoveAll(testDir)
|
||||||
|
config.Cfg.ExportTempDir = "../xporttemp"
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create test directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.New()
|
||||||
|
router.GET("/file/:filename", GetPDFFile)
|
||||||
|
|
||||||
|
// Act - Try path traversal attack
|
||||||
|
req, _ := http.NewRequest("GET", "/file/../../../etc/passwd", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Assert - Should return error (either 404 or 400)
|
||||||
|
if w.Code == http.StatusOK {
|
||||||
|
t.Error("Should not allow path traversal attacks")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanupEndpoint(t *testing.T) {
|
||||||
|
setupTestEnvironment(t)
|
||||||
|
|
||||||
|
// Create test directory with old and new files
|
||||||
|
testDir := "../xporttemp_test_cleanup_endpoint"
|
||||||
|
config.Cfg.ExportTempDir = testDir
|
||||||
|
defer func() {
|
||||||
|
os.RemoveAll(testDir)
|
||||||
|
config.Cfg.ExportTempDir = "../xporttemp"
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(testDir, 0755); err != nil {
|
||||||
|
t.Fatalf("Failed to create test directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Create old file (8 days old)
|
||||||
|
oldFile := filepath.Join(testDir, "old_file.pdf")
|
||||||
|
if err := os.WriteFile(oldFile, []byte("%PDF-old"), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create old file: %v", err)
|
||||||
|
}
|
||||||
|
oldTime := now.Add(-8 * 24 * time.Hour)
|
||||||
|
if err := os.Chtimes(oldFile, oldTime, oldTime); err != nil {
|
||||||
|
t.Fatalf("Failed to set old file time: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create recent file (3 days old)
|
||||||
|
recentFile := filepath.Join(testDir, "recent_file.pdf")
|
||||||
|
if err := os.WriteFile(recentFile, []byte("%PDF-recent"), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to create recent file: %v", err)
|
||||||
|
}
|
||||||
|
recentTime := now.Add(-3 * 24 * time.Hour)
|
||||||
|
if err := os.Chtimes(recentFile, recentTime, recentTime); err != nil {
|
||||||
|
t.Fatalf("Failed to set recent file time: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.New()
|
||||||
|
router.POST("/cleanup", CleanupExportTemp)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
req, _ := http.NewRequest("POST", "/cleanup", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
var response map[string]interface{}
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
|
||||||
|
t.Fatalf("Failed to parse JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check deleted count
|
||||||
|
deletedCount, ok := response["deleted"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Response should contain 'deleted' field")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have deleted 1 file (the old one)
|
||||||
|
if int(deletedCount.(float64)) != 1 {
|
||||||
|
t.Errorf("Expected 1 file deleted, got %v", deletedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify old file is deleted
|
||||||
|
if _, err := os.Stat(oldFile); !os.IsNotExist(err) {
|
||||||
|
t.Error("Old file should be deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify recent file still exists
|
||||||
|
if _, err := os.Stat(recentFile); err != nil {
|
||||||
|
t.Error("Recent file should still exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,12 +4,22 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RegisterRoutes registers protected PDF routes
|
||||||
func RegisterRoutes(r *gin.RouterGroup) {
|
func RegisterRoutes(r *gin.RouterGroup) {
|
||||||
pdfGrp := r.Group("/pdf")
|
pdfGrp := r.Group("/pdf")
|
||||||
|
|
||||||
// List available templates
|
// List available templates (protected)
|
||||||
pdfGrp.GET("/templates", ListTemplates)
|
pdfGrp.GET("/templates", ListTemplates)
|
||||||
|
|
||||||
// Export character to PDF
|
// Export character to PDF (protected)
|
||||||
pdfGrp.GET("/export/:id", ExportCharacterToPDF)
|
pdfGrp.GET("/export/:id", ExportCharacterToPDF)
|
||||||
|
|
||||||
|
// Cleanup old PDF files (protected)
|
||||||
|
pdfGrp.POST("/cleanup", CleanupExportTemp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterPublicRoutes registers public PDF routes (no authentication required)
|
||||||
|
func RegisterPublicRoutes(r *gin.Engine) {
|
||||||
|
// Get PDF file from xporttemp (public - for direct browser access)
|
||||||
|
r.GET("/api/pdf/file/:filename", GetPDFFile)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,4 +104,5 @@
|
|||||||
|
|
||||||
* currently the template fetched for rendering is set to Default_A4_Quer
|
* currently the template fetched for rendering is set to Default_A4_Quer
|
||||||
* remove inline css as far as possible
|
* remove inline css as far as possible
|
||||||
* make pdf download popup an own view
|
* make pdf download popup an own view
|
||||||
|
* func CleanupExportTemp move maxAge := 7 * 24 * time.Hour definition to Config struct n config.go
|
||||||
@@ -409,65 +409,6 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open window IMMEDIATELY (synchronously) to avoid popup blocker
|
|
||||||
const pdfWindow = window.open('', '_blank')
|
|
||||||
if (!pdfWindow) {
|
|
||||||
alert(this.$t('export.popupBlocked'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show loading page in the new window
|
|
||||||
pdfWindow.document.write(`
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>${this.$t('export.generating')}</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100vh;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
.loading-container {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.spinner {
|
|
||||||
border: 4px solid #f3f3f3;
|
|
||||||
border-top: 4px solid #007bff;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin: 0 auto 20px;
|
|
||||||
}
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
color: #333;
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
color: #666;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="loading-container">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<h2>${this.$t('export.generating')}</h2>
|
|
||||||
<p>${this.$t('export.pleaseWait')}</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`)
|
|
||||||
|
|
||||||
this.isExporting = true
|
this.isExporting = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -479,27 +420,24 @@ export default {
|
|||||||
params.append('showUserName', 'true')
|
params.append('showUserName', 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch PDF from API
|
// Get filename from export API (saves PDF to file)
|
||||||
const response = await API.get(`/api/pdf/export/${this.id}`, {
|
const response = await API.get(`/api/pdf/export/${this.id}`, {
|
||||||
params: Object.fromEntries(params),
|
params: Object.fromEntries(params)
|
||||||
responseType: 'blob'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create object URL from blob
|
const filename = response.data.filename
|
||||||
const blob = new Blob([response.data], { type: 'application/pdf' })
|
if (!filename) {
|
||||||
const url = window.URL.createObjectURL(blob)
|
throw new Error('No filename returned from export')
|
||||||
|
}
|
||||||
|
|
||||||
// Replace loading page with PDF
|
// Open PDF in new window using file endpoint
|
||||||
pdfWindow.location.href = url
|
const pdfUrl = `${API.defaults.baseURL}/api/pdf/file/${filename}`
|
||||||
|
window.open(pdfUrl, '_blank')
|
||||||
// Clean up blob URL after some time
|
|
||||||
setTimeout(() => window.URL.revokeObjectURL(url), 10000)
|
|
||||||
|
|
||||||
// Close dialog on success
|
// Close dialog on success
|
||||||
this.showExportDialog = false
|
this.showExportDialog = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to export PDF:', error)
|
console.error('Failed to export PDF:', error)
|
||||||
pdfWindow.close()
|
|
||||||
alert(this.$t('export.exportFailed') + ': ' + (error.response?.data?.error || error.message))
|
alert(this.$t('export.exportFailed') + ': ' + (error.response?.data?.error || error.message))
|
||||||
} finally {
|
} finally {
|
||||||
this.isExporting = false
|
this.isExporting = false
|
||||||
|
|||||||
Reference in New Issue
Block a user