PDF generation and downloads are working

This commit is contained in:
2025-12-21 09:15:08 +01:00
parent 2af477397e
commit cd0f98042d
10 changed files with 739 additions and 131 deletions
+1
View File
@@ -43,3 +43,4 @@ maintenance/testdata/*
testdata/*_data.db
tmp/main
uploads/*
xporttemp/*
+4
View File
@@ -68,6 +68,10 @@ func main() {
maintenance.RegisterRoutes(protected)
importer.RegisterRoutes(protected)
pdfrender.RegisterRoutes(protected)
// Register public routes (no authentication)
pdfrender.RegisterPublicRoutes(r)
logger.Info("API-Routen erfolgreich registriert")
// Server starten
+16 -9
View File
@@ -27,7 +27,8 @@ type Config struct {
DevTesting string // "yes" or "no", used to determine if we are in a test environment
// 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
@@ -42,14 +43,15 @@ func init() {
// defaultConfig gibt die Standard-Konfiguration zurück
func defaultConfig() *Config {
return &Config{
ServerPort: "8180",
DatabaseURL: "",
DatabaseType: "mysql",
DebugMode: false,
LogLevel: "INFO",
Environment: "production",
DevTesting: "no", // Default to "no", can be overridden in tests
TemplatesDir: "./templates", // Default templates directory
ServerPort: "8180",
DatabaseURL: "",
DatabaseType: "mysql",
DebugMode: false,
LogLevel: "INFO",
Environment: "production",
DevTesting: "no", // Default to "no", can be overridden in tests
TemplatesDir: "./templates", // Default templates directory
ExportTempDir: "./xporttemp", // Default export temp directory
}
}
@@ -120,6 +122,11 @@ func LoadConfig() *Config {
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",
config.Environment, config.DevTesting, config.DatabaseType)
+82
View File
@@ -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
}
+182
View File
@@ -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")
}
}
+98 -33
View File
@@ -45,10 +45,12 @@ func ListTemplates(c *gin.Context) {
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:
// - template: template ID to use (default: "Default_A4_Quer")
// - showUserName: whether to show user name (default: false)
//
// Returns JSON with filename: {"filename": "CharacterName_20231225_143045.pdf"}
func ExportCharacterToPDF(c *gin.Context) {
// Get character ID
charID := c.Param("id")
@@ -120,47 +122,110 @@ func ExportCharacterToPDF(c *gin.Context) {
}
allPDFs = append(allPDFs, page4PDFs...)
// If only one PDF, return it directly
// Merge PDFs if needed
var finalPDF []byte
if len(allPDFs) == 1 {
c.Data(http.StatusOK, "application/pdf", allPDFs[0])
return
}
// Merge multiple PDFs
tmpDir := "/tmp/bamort_pdf_export"
if err := os.MkdirAll(tmpDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create temp directory"})
return
}
defer os.RemoveAll(tmpDir)
// Save individual PDFs
var filePaths []string
for i, pdf := range allPDFs {
filename := fmt.Sprintf("%s/page_%d.pdf", tmpDir, i)
if err := os.WriteFile(filename, pdf, 0644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to write temporary PDF"})
finalPDF = allPDFs[0]
} else {
// Merge multiple PDFs
tmpDir := "/tmp/bamort_pdf_export"
if err := os.MkdirAll(tmpDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create temp directory"})
return
}
defer os.RemoveAll(tmpDir)
// Save individual PDFs
var filePaths []string
for i, pdf := range allPDFs {
filename := fmt.Sprintf("%s/page_%d.pdf", tmpDir, i)
if err := os.WriteFile(filename, pdf, 0644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to write temporary PDF"})
return
}
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
}
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()})
// Ensure export temp directory exists
if err := EnsureExportTempDir(config.Cfg.ExportTempDir); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create export directory"})
return
}
// Read combined PDF
combinedPDF, err := os.ReadFile(combinedPath)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read combined PDF"})
// Generate filename
filename := GenerateExportFilename(char.Name, time.Now())
filePath := filepath.Join(config.Cfg.ExportTempDir, filename)
// 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
}
// Set response headers
filename := fmt.Sprintf("%s_character_sheet.pdf", char.Name)
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
c.Data(http.StatusOK, "application/pdf", combinedPDF)
// Return filename
c.JSON(http.StatusOK, gin.H{"filename": filename})
}
// 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),
})
}
+332 -14
View File
@@ -7,7 +7,11 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
)
@@ -15,6 +19,7 @@ import (
func init() {
// Set templates directory for tests (tests run from pdfrender/ directory)
config.Cfg.TemplatesDir = "../templates"
config.Cfg.ExportTempDir = "../xporttemp"
}
func TestListTemplates(t *testing.T) {
@@ -59,6 +64,14 @@ func TestExportCharacterToPDF(t *testing.T) {
// Arrange
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
char := &models.Char{}
err := char.FirstID("18")
@@ -77,24 +90,41 @@ func TestExportCharacterToPDF(t *testing.T) {
// Assert
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")
if contentType != "application/pdf" {
t.Errorf("Expected Content-Type 'application/pdf', got '%s'", contentType)
if !strings.Contains(contentType, "application/json") {
t.Errorf("Expected Content-Type 'application/json', got '%s'", contentType)
}
// Check PDF content
body := w.Body.Bytes()
if len(body) == 0 {
t.Error("Expected non-empty PDF content")
// 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)
}
// 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
if string(body[0:4]) != "%PDF" {
t.Error("Response does not start with PDF marker")
if string(data[0:4]) != "%PDF" {
t.Error("File does not start with PDF marker")
}
}
@@ -102,6 +132,14 @@ func TestExportCharacterToPDF_WithTemplate(t *testing.T) {
// Arrange
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)
router := gin.New()
router.GET("/export/:id", ExportCharacterToPDF)
@@ -113,13 +151,26 @@ func TestExportCharacterToPDF_WithTemplate(t *testing.T) {
// Assert
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
body := w.Body.Bytes()
if string(body[0:4]) != "%PDF" {
t.Error("Response does not start with PDF marker")
data, err := os.ReadFile(filePath)
if err != nil {
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)
}
}
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")
}
}
+12 -2
View File
@@ -4,12 +4,22 @@ import (
"github.com/gin-gonic/gin"
)
// RegisterRoutes registers protected PDF routes
func RegisterRoutes(r *gin.RouterGroup) {
pdfGrp := r.Group("/pdf")
// List available templates
// List available templates (protected)
pdfGrp.GET("/templates", ListTemplates)
// Export character to PDF
// Export character to PDF (protected)
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)
}
+1
View File
@@ -105,3 +105,4 @@
* currently the template fetched for rendering is set to Default_A4_Quer
* remove inline css as far as possible
* make pdf download popup an own view
* func CleanupExportTemp move maxAge := 7 * 24 * time.Hour definition to Config struct n config.go
+9 -71
View File
@@ -409,65 +409,6 @@ export default {
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
try {
@@ -479,27 +420,24 @@ export default {
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}`, {
params: Object.fromEntries(params),
responseType: 'blob'
params: Object.fromEntries(params)
})
// Create object URL from blob
const blob = new Blob([response.data], { type: 'application/pdf' })
const url = window.URL.createObjectURL(blob)
const filename = response.data.filename
if (!filename) {
throw new Error('No filename returned from export')
}
// Replace loading page with PDF
pdfWindow.location.href = url
// Clean up blob URL after some time
setTimeout(() => window.URL.revokeObjectURL(url), 10000)
// Open PDF in new window using file endpoint
const pdfUrl = `${API.defaults.baseURL}/api/pdf/file/${filename}`
window.open(pdfUrl, '_blank')
// Close dialog on success
this.showExportDialog = false
} catch (error) {
console.error('Failed to export PDF:', error)
pdfWindow.close()
alert(this.$t('export.exportFailed') + ': ' + (error.response?.data?.error || error.message))
} finally {
this.isExporting = false