diff --git a/.gitignore b/.gitignore index a272658..406784b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ keys/ .idea/ *.kate-swp + +# Diagram cache directory +media/diagram_cache/ diff --git a/Documentation/DIAGRAM_CACHING.md b/Documentation/DIAGRAM_CACHING.md new file mode 100644 index 0000000..4effbbf --- /dev/null +++ b/Documentation/DIAGRAM_CACHING.md @@ -0,0 +1,105 @@ +# Diagram POST Caching Implementation + +This feature replaces the URL-encoded GET approach for diagram generation with POST requests and local filesystem caching. + +## Changes Overview + +### New Files +- `diagramm_proxy/__init__.py` - Module initialization +- `diagramm_proxy/diagram_cache.py` - Caching logic and POST request handling +- `abschnitte/management/commands/clear_diagram_cache.py` - Management command for cache clearing + +### Modified Files +- `abschnitte/utils.py` - Updated `render_textabschnitte()` to use caching +- `.gitignore` - Added cache directory exclusion + +## Configuration Required + +Add to your Django settings file (e.g., `VorgabenUI/settings.py`): + +```python +# Diagram cache settings +DIAGRAM_CACHE_DIR = 'diagram_cache' # relative to MEDIA_ROOT + +# Ensure MEDIA_ROOT and MEDIA_URL are configured +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_URL = '/media/' +``` + +### URL Configuration + +Ensure media files are served in development. In your main `urls.py`: + +```python +from django.conf import settings +from django.conf.urls.static import static + +# ... existing urlpatterns ... + +# Serve media files in development +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +``` + +## How It Works + +1. When a diagram is rendered, the system computes a SHA256 hash of the diagram content +2. It checks if a cached SVG exists for that hash +3. If cached: serves the existing file +4. If not cached: POSTs content to Kroki server, saves the response, and serves it +5. Diagrams are served from `MEDIA_URL/diagram_cache/{type}/{hash}.svg` + +## Benefits + +- **No URL length limitations** - Content is POSTed instead of URL-encoded +- **Improved performance** - Cached diagrams are served directly from filesystem +- **Reduced server load** - Kroki server is only called once per unique diagram +- **Persistent cache** - Survives application restarts +- **Better error handling** - Graceful fallback on generation failures + +## Usage + +### Viewing Diagrams +No changes required - diagrams will be automatically cached on first render. + +### Clearing Cache + +Clear all cached diagrams: +```bash +python manage.py clear_diagram_cache +``` + +Clear diagrams of a specific type: +```bash +python manage.py clear_diagram_cache --type plantuml +python manage.py clear_diagram_cache --type mermaid +``` + +## Testing + +1. Create or view a page with diagrams +2. Verify diagrams render correctly +3. Check that `media/diagram_cache/` directory is created with cached SVGs +4. Refresh the page - second load should be faster (cache hit) +5. Check logs for cache hit/miss messages +6. Test cache clearing command + +## Migration Notes + +- Existing diagrams will be regenerated on first view after deployment +- The old URL-based approach is completely replaced +- No database migrations needed +- Ensure `requests` library is installed (already in requirements.txt) + +## Troubleshooting + +### Diagrams not rendering +- Check that MEDIA_ROOT and MEDIA_URL are configured correctly +- Verify Kroki server is accessible at `http://svckroki:8000` +- Check application logs for error messages +- Ensure media directory is writable + +### Cache not working +- Verify Django storage configuration +- Check file permissions on media/diagram_cache directory +- Review logs for cache-related errors diff --git a/abschnitte/management/__init__.py b/abschnitte/management/__init__.py new file mode 100644 index 0000000..2c1c7c1 --- /dev/null +++ b/abschnitte/management/__init__.py @@ -0,0 +1 @@ +# Management commands diff --git a/abschnitte/management/commands/__init__.py b/abschnitte/management/commands/__init__.py new file mode 100644 index 0000000..b5a3a84 --- /dev/null +++ b/abschnitte/management/commands/__init__.py @@ -0,0 +1 @@ +# Commands package diff --git a/abschnitte/management/commands/clear_diagram_cache.py b/abschnitte/management/commands/clear_diagram_cache.py new file mode 100644 index 0000000..035b466 --- /dev/null +++ b/abschnitte/management/commands/clear_diagram_cache.py @@ -0,0 +1,23 @@ +from django.core.management.base import BaseCommand +from diagramm_proxy.diagram_cache import clear_cache + + +class Command(BaseCommand): + help = 'Clear cached diagrams' + + def add_arguments(self, parser): + parser.add_argument( + '--type', + type=str, + help='Diagram type to clear (e.g., plantuml, mermaid)', + ) + + def handle(self, *args, **options): + diagram_type = options.get('type') + if diagram_type: + self.stdout.write(f'Clearing cache for {diagram_type}...') + clear_cache(diagram_type) + else: + self.stdout.write('Clearing all diagram caches...') + clear_cache() + self.stdout.write(self.style.SUCCESS('Cache cleared successfully')) diff --git a/abschnitte/utils.py b/abschnitte/utils.py index 6194727..6d13cf4 100644 --- a/abschnitte/utils.py +++ b/abschnitte/utils.py @@ -3,6 +3,10 @@ import base64 import zlib import re from textwrap import dedent +from django.conf import settings + +# Import the caching function +from diagramm_proxy.diagram_cache import get_cached_diagram DIAGRAMMSERVER="/diagramm" @@ -25,15 +29,23 @@ def render_textabschnitte(queryset): elif typ == "tabelle": html = md_table_to_html(inhalt) elif typ == "diagramm": - temp=inhalt.splitlines() - diagramtype=temp.pop(0) - diagramoptions='width="100%"' - if temp[0][0:6].lower() == "option": - diagramoptions=temp.pop(0).split(":",1)[1] - rest="\n".join(temp) - html = '
Error generating diagram: {str(e)}
' + elif typ == "code": html = ""
html += markdown(inhalt, extensions=['tables', 'attr_list'])
diff --git a/diagramm_proxy/__init__.py b/diagramm_proxy/__init__.py
new file mode 100644
index 0000000..30fabe8
--- /dev/null
+++ b/diagramm_proxy/__init__.py
@@ -0,0 +1 @@
+# Diagram proxy module
diff --git a/diagramm_proxy/diagram_cache.py b/diagramm_proxy/diagram_cache.py
new file mode 100644
index 0000000..49e3b76
--- /dev/null
+++ b/diagramm_proxy/diagram_cache.py
@@ -0,0 +1,91 @@
+import hashlib
+import os
+import requests
+from pathlib import Path
+from django.conf import settings
+from django.core.files.storage import default_storage
+from django.core.files.base import ContentFile
+import logging
+
+logger = logging.getLogger(__name__)
+
+# Configure cache directory
+CACHE_DIR = getattr(settings, 'DIAGRAM_CACHE_DIR', 'diagram_cache')
+KROKI_UPSTREAM = "http://svckroki:8000"
+
+def get_cache_path(diagram_type, content_hash):
+ """Generate cache file path for a diagram."""
+ return os.path.join(CACHE_DIR, diagram_type, f"{content_hash}.svg")
+
+def compute_hash(content):
+ """Compute SHA256 hash of diagram content."""
+ return hashlib.sha256(content.encode('utf-8')).hexdigest()
+
+def get_cached_diagram(diagram_type, diagram_content):
+ """
+ Retrieve diagram from cache or generate it via POST.
+
+ Args:
+ diagram_type: Type of diagram (e.g., 'plantuml', 'mermaid')
+ diagram_content: Raw diagram content
+
+ Returns:
+ Path to cached diagram file (relative to MEDIA_ROOT)
+ """
+ content_hash = compute_hash(diagram_content)
+ cache_path = get_cache_path(diagram_type, content_hash)
+
+ # Check if diagram exists in cache
+ if default_storage.exists(cache_path):
+ logger.debug(f"Cache hit for {diagram_type} diagram: {content_hash[:8]}")
+ return cache_path
+
+ # Generate diagram via POST request
+ logger.info(f"Cache miss for {diagram_type} diagram: {content_hash[:8]}, generating...")
+ try:
+ url = f"{KROKI_UPSTREAM}/{diagram_type}/svg"
+ response = requests.post(
+ url,
+ data=diagram_content.encode('utf-8'),
+ headers={'Content-Type': 'text/plain'},
+ timeout=30
+ )
+ response.raise_for_status()
+
+ # Ensure cache directory exists
+ cache_dir = os.path.dirname(default_storage.path(cache_path))
+ os.makedirs(cache_dir, exist_ok=True)
+
+ # Save to cache
+ default_storage.save(cache_path, ContentFile(response.content))
+ logger.info(f"Diagram cached successfully: {cache_path}")
+
+ return cache_path
+
+ except requests.RequestException as e:
+ logger.error(f"Error generating diagram: {e}")
+ raise
+
+def clear_cache(diagram_type=None):
+ """
+ Clear cached diagrams.
+
+ Args:
+ diagram_type: If specified, only clear diagrams of this type
+ """
+ if diagram_type:
+ cache_path = os.path.join(CACHE_DIR, diagram_type)
+ else:
+ cache_path = CACHE_DIR
+
+ if default_storage.exists(cache_path):
+ full_path = default_storage.path(cache_path)
+ # Walk through and delete files
+ for root, dirs, files in os.walk(full_path):
+ for file in files:
+ file_path = os.path.join(root, file)
+ try:
+ os.remove(file_path)
+ logger.info(f"Deleted cached diagram: {file_path}")
+ except OSError as e:
+ logger.error(f"Error deleting {file_path}: {e}")