fix: eingefügte Bilder beim ersten Speichern dauerhaft ablegen
This commit is contained in:
@@ -5,7 +5,7 @@ class DocumentationConfig(PluginConfig):
|
||||
name = "netbox_documentation"
|
||||
verbose_name = "NetBox Dokumentation"
|
||||
description = "Wiki und Office-Dokumentation direkt in NetBox"
|
||||
version = "0.7.12"
|
||||
version = "0.7.13"
|
||||
author = "LKE"
|
||||
base_url = "documentation"
|
||||
min_version = "4.0.0"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import base64
|
||||
from io import BytesIO
|
||||
import re
|
||||
|
||||
|
||||
class EmbeddedImageError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
DATA_IMAGE_RE = re.compile(
|
||||
r"(?P<prefix>src\s*=\s*(?P<quote>['\"]))"
|
||||
r"data:(?P<mime>image/(?:png|jpeg|gif|webp));base64,(?P<data>[^'\"]+)"
|
||||
r"(?P=quote)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _decode(match):
|
||||
payload = re.sub(r"\s+", "", match.group("data"))
|
||||
try:
|
||||
content = base64.b64decode(payload, validate=True)
|
||||
except (ValueError, TypeError) as exc:
|
||||
raise EmbeddedImageError("Ein eingefügtes Bild enthält ungültige Base64-Daten.") from exc
|
||||
try:
|
||||
from PIL import Image
|
||||
image = Image.open(BytesIO(content))
|
||||
image.verify()
|
||||
except Exception as exc:
|
||||
raise EmbeddedImageError("Ein eingefügter Inhalt ist keine gültige Bilddatei.") from exc
|
||||
return match.group("mime").lower(), content
|
||||
|
||||
|
||||
def validate_embedded_images(html, max_total_bytes):
|
||||
total = 0
|
||||
for match in DATA_IMAGE_RE.finditer(html or ""):
|
||||
_, content = _decode(match)
|
||||
total += len(content)
|
||||
if total > max_total_bytes:
|
||||
raise EmbeddedImageError("Die eingefügten Bilder überschreiten die erlaubte Gesamtgröße.")
|
||||
|
||||
|
||||
def store_embedded_images(html, max_total_bytes, store):
|
||||
total = 0
|
||||
|
||||
def replace(match):
|
||||
nonlocal total
|
||||
mime, content = _decode(match)
|
||||
total += len(content)
|
||||
if total > max_total_bytes:
|
||||
raise EmbeddedImageError("Die eingefügten Bilder überschreiten die erlaubte Gesamtgröße.")
|
||||
url = store(mime, content)
|
||||
return f'{match.group("prefix")}{url}{match.group("quote")}'
|
||||
|
||||
return DATA_IMAGE_RE.sub(replace, html or "")
|
||||
@@ -1,6 +1,7 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db.models import Q
|
||||
from django.utils.html import format_html
|
||||
from utilities.forms import get_field_value
|
||||
@@ -11,7 +12,8 @@ from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import HTMXSelect
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from dcim.models import Device
|
||||
from .models import Document, DocumentAssignment, DocumentCategory
|
||||
from .embedded_images import EmbeddedImageError, store_embedded_images, validate_embedded_images
|
||||
from .models import Document, DocumentAssignment, DocumentAttachment, DocumentCategory
|
||||
|
||||
|
||||
def help_label(label, description):
|
||||
@@ -59,6 +61,39 @@ class DocumentForm(NetBoxModelForm):
|
||||
self.initial["body"] = markdown(self.instance.body, extensions=("extra", "sane_lists"))
|
||||
self.initial["body_format"] = "html"
|
||||
|
||||
def clean_body(self):
|
||||
body = self.cleaned_data.get("body", "")
|
||||
limit = settings.PLUGINS_CONFIG.get("netbox_documentation", {}).get("max_import_size_mb", 25)
|
||||
try:
|
||||
validate_embedded_images(body, limit * 1024 * 1024)
|
||||
except EmbeddedImageError as exc:
|
||||
raise forms.ValidationError(str(exc)) from exc
|
||||
return body
|
||||
|
||||
def save(self, commit=True):
|
||||
document = super().save(commit=commit)
|
||||
if not commit or not document.pk or "data:image/" not in (document.body or "").lower():
|
||||
return document
|
||||
|
||||
limit = settings.PLUGINS_CONFIG.get("netbox_documentation", {}).get("max_import_size_mb", 25)
|
||||
counter = 0
|
||||
|
||||
def save_image(mime, content):
|
||||
nonlocal counter
|
||||
counter += 1
|
||||
extension = {"image/jpeg": "jpg", "image/png": "png", "image/gif": "gif", "image/webp": "webp"}[mime]
|
||||
name = f"editor-bild-{document.pk}-{counter}.{extension}"
|
||||
attachment = DocumentAttachment(
|
||||
document=document, original_name=name, content_type=mime, size=len(content),
|
||||
)
|
||||
attachment.file.save(name, ContentFile(content), save=False)
|
||||
attachment.save()
|
||||
return attachment.file.url
|
||||
|
||||
document.body = store_embedded_images(document.body, limit * 1024 * 1024, save_image)
|
||||
document.save(update_fields=("body", "last_updated"))
|
||||
return document
|
||||
|
||||
|
||||
class AssignmentForm(NetBoxModelForm):
|
||||
assigned_object_type = ContentTypeChoiceField(queryset=ContentType.objects.none(), label="Objekttyp", widget=HTMXSelect())
|
||||
|
||||
@@ -175,6 +175,6 @@
|
||||
|
||||
{% block pre_form_fields %}
|
||||
{% if not object.pk %}
|
||||
<div class="alert alert-info">Speichere die neue Dokumentation einmal. Danach können Bilder direkt über die Editor-Werkzeugleiste hochgeladen werden.</div>
|
||||
<div class="alert alert-info">Eingefügte oder hineinkopierte Bilder werden beim ersten Speichern automatisch als Medien der neuen Dokumentation abgelegt. Nach dem ersten Speichern ist zusätzlich der direkte Bildupload verfügbar.</div>
|
||||
{% endif %}
|
||||
{% endblock pre_form_fields %}
|
||||
|
||||
Reference in New Issue
Block a user