diff --git a/backend/cmd/main.go b/backend/cmd/main.go index c9760e2..914fae8 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -56,6 +56,14 @@ func main() { database.ConnectDatabase() logger.Info("Datenbankverbindung erfolgreich") + // Initialize PDF templates + logger.Debug("Initialisiere PDF-Templates...") + if err := pdfrender.InitializeTemplates("/app/default_templates", cfg.TemplatesDir); err != nil { + logger.Warn("Fehler beim Initialisieren der Templates: %s", err.Error()) + } else { + logger.Info("PDF-Templates erfolgreich initialisiert") + } + r := gin.Default() router.SetupGin(r) diff --git a/backend/pdfrender/init.go b/backend/pdfrender/init.go new file mode 100644 index 0000000..641df48 --- /dev/null +++ b/backend/pdfrender/init.go @@ -0,0 +1,175 @@ +package pdfrender + +import ( + "bamort/logger" + "fmt" + "io" + "os" + "path/filepath" +) + +// InitializeTemplates copies default templates to target directory if they don't exist +// or if the content has changed (for development updates) +// This should be called once during application startup +func InitializeTemplates(defaultDir, targetDir string) error { + // Check if default directory exists + if _, err := os.Stat(defaultDir); os.IsNotExist(err) { + return fmt.Errorf("default templates directory does not exist: %s", defaultDir) + } + + // Read default templates + entries, err := os.ReadDir(defaultDir) + if err != nil { + return fmt.Errorf("failed to read default templates: %w", err) + } + + // Process each template directory + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + srcDir := filepath.Join(defaultDir, entry.Name()) + dstDir := filepath.Join(targetDir, entry.Name()) + + // Copy or update template directory + updated, err := syncDir(srcDir, dstDir) + if err != nil { + return fmt.Errorf("failed to sync template %s: %w", entry.Name(), err) + } + + if updated { + logger.Info("Initialized/updated template: %s", entry.Name()) + } else { + logger.Debug("Template %s is up to date", entry.Name()) + } + } + + return nil +} + +// syncDir synchronizes source directory to destination, copying only changed files +// Returns true if any files were updated +func syncDir(src, dst string) (bool, error) { + // Get source directory info + srcInfo, err := os.Stat(src) + if err != nil { + return false, err + } + + // Create destination directory if it doesn't exist + if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil { + return false, err + } + + // Read source directory entries + entries, err := os.ReadDir(src) + if err != nil { + return false, err + } + + updated := false + // Copy each entry + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + // Recursively sync subdirectory + subUpdated, err := syncDir(srcPath, dstPath) + if err != nil { + return false, err + } + if subUpdated { + updated = true + } + } else { + // Check if file needs to be copied + needsCopy, err := fileNeedsUpdate(srcPath, dstPath) + if err != nil { + return false, err + } + + if needsCopy { + if err := copyFile(srcPath, dstPath); err != nil { + return false, err + } + updated = true + } + } + } + + return updated, nil +} + +// fileNeedsUpdate checks if destination file is missing or differs from source +func fileNeedsUpdate(src, dst string) (bool, error) { + // If destination doesn't exist, needs update + dstInfo, err := os.Stat(dst) + if os.IsNotExist(err) { + return true, nil + } + if err != nil { + return false, err + } + + // Get source info + srcInfo, err := os.Stat(src) + if err != nil { + return false, err + } + + // Quick check: if sizes differ, files differ + if srcInfo.Size() != dstInfo.Size() { + return true, nil + } + + // Compare file contents + return filesContentDiffer(src, dst) +} + +// filesContentDiffer compares file contents +func filesContentDiffer(file1, file2 string) (bool, error) { + content1, err := os.ReadFile(file1) + if err != nil { + return false, err + } + + content2, err := os.ReadFile(file2) + if err != nil { + return false, err + } + + // Files differ if contents don't match + return string(content1) != string(content2), nil +} + +// copyFile copies a single file +func copyFile(src, dst string) error { + // Open source file + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + + // Get source file info + srcInfo, err := srcFile.Stat() + if err != nil { + return err + } + + // Create destination file + dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode()) + if err != nil { + return err + } + defer dstFile.Close() + + // Copy contents + if _, err := io.Copy(dstFile, srcFile); err != nil { + return err + } + + return nil +} diff --git a/backend/pdfrender/init_test.go b/backend/pdfrender/init_test.go new file mode 100644 index 0000000..cfb93eb --- /dev/null +++ b/backend/pdfrender/init_test.go @@ -0,0 +1,105 @@ +package pdfrender + +import ( + "os" + "path/filepath" + "testing" +) + +func TestInitializeTemplates(t *testing.T) { + // Setup test directories + tmpDir := t.TempDir() + defaultDir := filepath.Join(tmpDir, "default_templates") + targetDir := filepath.Join(tmpDir, "templates") + + // Create default templates directory with test files + if err := os.MkdirAll(filepath.Join(defaultDir, "TestTemplate"), 0755); err != nil { + t.Fatalf("Failed to create default template dir: %v", err) + } + testFile := filepath.Join(defaultDir, "TestTemplate", "page1.html") + if err := os.WriteFile(testFile, []byte("test content"), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Test 1: Copy when target directory doesn't exist + if err := InitializeTemplates(defaultDir, targetDir); err != nil { + t.Errorf("InitializeTemplates failed: %v", err) + } + + // Verify file was copied + copiedFile := filepath.Join(targetDir, "TestTemplate", "page1.html") + if _, err := os.Stat(copiedFile); os.IsNotExist(err) { + t.Error("Expected file was not copied") + } + + content, err := os.ReadFile(copiedFile) + if err != nil || string(content) != "test content" { + t.Error("Copied file content doesn't match") + } + + // Test 2: Don't overwrite when content is identical + if err := InitializeTemplates(defaultDir, targetDir); err != nil { + t.Errorf("InitializeTemplates failed on second run: %v", err) + } + + // Verify file still has same content + content, err = os.ReadFile(copiedFile) + if err != nil || string(content) != "test content" { + t.Error("File content should remain unchanged") + } + + // Test 3: Update when default template changed + updatedContent := []byte("updated test content") + if err := os.WriteFile(testFile, updatedContent, 0644); err != nil { + t.Fatalf("Failed to update source file: %v", err) + } + + if err := InitializeTemplates(defaultDir, targetDir); err != nil { + t.Errorf("InitializeTemplates failed after source update: %v", err) + } + + // Verify file was updated + content, err = os.ReadFile(copiedFile) + if err != nil || string(content) != "updated test content" { + t.Error("File should have been updated with new content") + } + + // Test 4: Handle missing default directory gracefully + if err := InitializeTemplates("/nonexistent", targetDir); err == nil { + t.Error("Expected error for nonexistent default directory") + } +} + +func TestInitializeTemplatesWithMultipleFiles(t *testing.T) { + tmpDir := t.TempDir() + defaultDir := filepath.Join(tmpDir, "default_templates") + targetDir := filepath.Join(tmpDir, "templates") + + // Create multiple templates and files + templates := []string{"Template1", "Template2"} + for _, tmpl := range templates { + tmplDir := filepath.Join(defaultDir, tmpl) + if err := os.MkdirAll(tmplDir, 0755); err != nil { + t.Fatalf("Failed to create template dir: %v", err) + } + for i := 1; i <= 3; i++ { + file := filepath.Join(tmplDir, filepath.Base(tmpl)+".html") + if err := os.WriteFile(file, []byte("content "+tmpl), 0644); err != nil { + t.Fatalf("Failed to create file: %v", err) + } + } + } + + // Initialize templates + if err := InitializeTemplates(defaultDir, targetDir); err != nil { + t.Fatalf("InitializeTemplates failed: %v", err) + } + + // Verify all templates were copied + for _, tmpl := range templates { + tmplDir := filepath.Join(targetDir, tmpl) + if _, err := os.Stat(tmplDir); os.IsNotExist(err) { + t.Errorf("Template directory %s was not copied", tmpl) + } + } +} diff --git a/docker/.gitignore b/docker/.gitignore index a0d979d..e343aff 100644 --- a/docker/.gitignore +++ b/docker/.gitignore @@ -1 +1,2 @@ -bamort-db* \ No newline at end of file +bamort-db* +templates \ No newline at end of file diff --git a/docker/Dockerfile.backend b/docker/Dockerfile.backend index 2945875..6b20502 100644 --- a/docker/Dockerfile.backend +++ b/docker/Dockerfile.backend @@ -37,6 +37,7 @@ WORKDIR /app # Copy the compiled binary from builder stage COPY --from=builder /app/server /app +COPY --from=builder /app/templates /app/default_templates # Expose port EXPOSE 8180 diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index afdfcda..f14ed57 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -1,4 +1,3 @@ -version: "3.8" services: backend-dev: build: @@ -11,9 +10,9 @@ services: - GO_ENV=development - CGO_ENABLED=1 - DATABASE_TYPE=mysql - - DATABASE_URL=bamort:bG4)efozrc@tcp(mariadb:3306)/bamort?charset=utf8mb4&parseTime=True&loc=Local + - DATABASE_URL=bamort:bG4)efozrc@tcp(mariadb-dev:3306)/bamort?charset=utf8mb4&parseTime=True&loc=Local depends_on: - mariadb: + mariadb-dev: condition: service_healthy working_dir: /app # Restart if Go code changes cause crash @@ -39,7 +38,7 @@ services: - ../frontend:/app - /app/node_modules # Prevent overwriting node_modules - mariadb: + mariadb-dev: image: mariadb:11.4 container_name: bamort-mariadb-dev restart: unless-stopped @@ -61,21 +60,21 @@ services: timeout: 5s retries: 3 - phpmyadmin: + phpmyadmin-dev: image: phpmyadmin/phpmyadmin:5.2 container_name: bamort-phpmyadmin-dev restart: unless-stopped ports: - "8081:80" environment: - PMA_HOST: mariadb + PMA_HOST: mariadb-dev PMA_PORT: 3306 PMA_USER: root PMA_PASSWORD: root_password_dev MYSQL_ROOT_PASSWORD: root_password_dev PMA_ARBITRARY: 1 depends_on: - mariadb: + mariadb-dev: condition: service_healthy volumes: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 2e8dde4..23e87eb 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3.8" services: backend: build: @@ -16,19 +15,17 @@ services: mariadb: condition: service_healthy working_dir: /app + volumes: + - ./templates:/app/templates restart: unless-stopped - - - - frontend: build: context: ../frontend dockerfile: ../docker/Dockerfile.frontend container_name: bamort-frontend ports: - - "5173:80" + - "80:80" environment: - ENVIRONMENT=production - VITE_API_URL=${VITE_API_URL:-http://bamort.trokan.de:8180} @@ -56,8 +53,8 @@ services: - ./init-db:/docker-entrypoint-initdb.d healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] - start_period: 30s - timeout: 15s + start_period: 20s + timeout: 10s retries: 3 # phpMyAdmin - Database Management (commented out for production)