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
+2 -1
View File
@@ -42,4 +42,5 @@ bamort
maintenance/testdata/*
testdata/*_data.db
tmp/main
uploads/*
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)
}
+2 -1
View File
@@ -104,4 +104,5 @@
* 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
* make pdf download popup an own view
* func CleanupExportTemp move maxAge := 7 * 24 * time.Hour definition to Config struct n config.go