Spaces:
Paused
Paused
Download iserv-filter/taskfilter.py from Luca448/APP-Backend: direct link, hf CLI and curl.
- Browser
- Download file 8.15 kB
-
https://huggingface.co/spaces/Luca448/APP-Backend/resolve/main/iserv-filter/taskfilter.py
- Command line
-
hf download hf://spaces/Luca448/APP-Backend/iserv-filter/taskfilter.py
-
curl -L -o taskfilter.py https://huggingface.co/spaces/Luca448/APP-Backend/resolve/main/iserv-filter/taskfilter.py
8.15 kB
| """filter_task: the one place that decides what leaves the server. Explicit whitelist, free text always filtered.""" | |
| import base64 | |
| import io | |
| import mimetypes | |
| import zipfile | |
| from collections import Counter | |
| from iserv import html_text | |
| from nerfilter import filter_filename, filter_text, split_ext | |
| READ_LIMIT = 20_000 # chars read from a file; bounds CPU time on huge PDFs | |
| MAX_TEXT = 3_000 # chars of a file that go out: worksheets are long and Muse needs the gist | |
| NOT_READABLE = "Inhalt nicht maschinell lesbar" | |
| FILE_WITHHELD = "Originaldatei nicht herausgegeben (Namen enthalten oder nicht prüfbar)" | |
| _MIME = {"pdf": "application/pdf", "txt": "text/plain", "md": "text/markdown", | |
| "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"} | |
| _MEDIA = {"jpg": "bild", "jpeg": "bild", "png": "bild", "gif": "bild", "webp": "bild", "bmp": "bild", "heic": "bild", | |
| "svg": "bild", "mp3": "audio", "wav": "audio", "m4a": "audio", "ogg": "audio", "mp4": "video", | |
| "mov": "video", "avi": "video", "mkv": "video", "webm": "video"} | |
| def _decode(data): | |
| try: | |
| return data.decode("utf-8") | |
| except UnicodeDecodeError: | |
| return data.decode("cp1252", errors="replace") | |
| def _pdf(data): | |
| from pypdf import PdfReader | |
| reader = PdfReader(io.BytesIO(data)) | |
| if reader.is_encrypted: | |
| return "" | |
| text = "" | |
| for page in reader.pages: | |
| text += (page.extract_text() or "") + "\n" | |
| if len(text) > READ_LIMIT: | |
| break | |
| return text | |
| def _docx(data): | |
| with zipfile.ZipFile(io.BytesIO(data)) as z: | |
| if sum(i.file_size for i in z.infolist()) > 50 * 1024 * 1024: # zip bomb | |
| return "" | |
| from docx import Document | |
| doc = Document(io.BytesIO(data)) | |
| cells = {} # a merged cell is returned once per grid column it spans: take each one only once | |
| for t in doc.tables: | |
| for r in t.rows: | |
| for c in r.cells: | |
| cells.setdefault(c._tc, c.text) | |
| return "\n".join([p.text for p in doc.paragraphs] + list(cells.values())) | |
| def _html(data): | |
| return html_text(_decode(data)) | |
| _EXTRACT = {"pdf": _pdf, "docx": _docx, "txt": _decode, "md": _decode, "html": _html, "htm": _html} | |
| def _tidy(text): | |
| """Drops what says nothing: blank lines, page numbers, a line identical to the one before (running headers).""" | |
| out = [] | |
| for line in (l.strip() for l in text.splitlines()): | |
| if line and not line.isdigit() and (not out or line != out[-1]): | |
| out.append(line) | |
| return "\n".join(out) | |
| def _docx_checked(data): | |
| """(text of headers and footers, cleaned bytes) of a docx, or None if it has comments (author names).""" | |
| with zipfile.ZipFile(io.BytesIO(data)) as z: | |
| if "word/comments.xml" in z.namelist(): | |
| return None | |
| from docx import Document | |
| doc = Document(io.BytesIO(data)) | |
| extra = "\n".join(p.text for s in doc.sections for part in (s.header, s.footer) for p in part.paragraphs) | |
| props = doc.core_properties | |
| for field in ("author", "last_modified_by", "title", "subject", "comments", "keywords", "category"): | |
| setattr(props, field, "") | |
| out = io.BytesIO() | |
| doc.save(out) | |
| return extra, out.getvalue() | |
| def _pdf_clean(data): | |
| """The pages only: no document info, bookmarks, forms or annotations (links, comments).""" | |
| from pypdf import PdfReader, PdfWriter | |
| writer = PdfWriter() | |
| for page in PdfReader(io.BytesIO(data)).pages: | |
| page.pop("/Annots", None) | |
| writer.add_page(page) | |
| out = io.BytesIO() | |
| writer.write(out) | |
| return out.getvalue() | |
| def _file_out(ext, data, text, stats=None): | |
| """(datei, filtered): {mime, base64} of the original file if nothing in it is a name (else None), and the filtered | |
| text, so the NER reads the file only once. Only what the NER can read is checked, so scans, images and audio never | |
| go out; a file cut at READ_LIMIT was not read completely (filtered is None: the caller filters the part it shows).""" | |
| if ext not in _MIME or not text or len(text) > READ_LIMIT: | |
| return None, None | |
| found = Counter() | |
| filtered = filter_text(text, found) | |
| if stats is not None: | |
| stats["replacements"] += found["replacements"] | |
| try: | |
| if ext == "docx": | |
| checked = _docx_checked(data) | |
| if checked is None: | |
| return None, filtered | |
| if checked[0].strip(): | |
| filter_text(checked[0], found) | |
| data = checked[1] | |
| elif ext == "pdf": | |
| data = _pdf_clean(data) | |
| except Exception: | |
| return None, filtered | |
| if found["replacements"]: | |
| return None, filtered | |
| return {"mime": _MIME[ext], "base64": base64.b64encode(data).decode()}, filtered | |
| def _unchecked(out, att, fid, release): | |
| """A file Muse may not see by itself: it gets an id to ask the user for. Only an id the user released | |
| (via the backend's /iserv/freigabe) hands out the original, as it is: it was not checked for names.""" | |
| out["datei_hinweis"] = FILE_WITHHELD | |
| out["datei"] = None | |
| if fid in release: | |
| out["datei"] = {"mime": mimetypes.guess_type(att.get("name") or "")[0] or "application/octet-stream", | |
| "base64": base64.b64encode(att["data"]).decode()} | |
| out["datei_hinweis"] = "Original auf Freigabe herausgegeben, nicht auf Namen geprüft" | |
| else: | |
| out["freigabe"] = fid | |
| return out | |
| def describe_attachment(att, stats=None, files=False, fid=None, release=()): | |
| """{name, typ, groesse, text | hinweis}; with files also datei (original file) or datei_hinweis + freigabe id. | |
| The file bytes leave this function only through _file_out and _unchecked.""" | |
| if att.get("skipped"): | |
| return {"hinweis": "weiterer Anhang nicht abgerufen"} | |
| name = att.get("name") or "" | |
| ext = split_ext(name)[1] | |
| out = {"name": filter_filename(name, stats), "typ": _MEDIA.get(ext) or ext or "unbekannt", "groesse": att.get("size")} | |
| if att.get("failed"): | |
| if stats is not None: | |
| stats["attachments_failed"] += 1 | |
| return {**out, "hinweis": "Datei nicht abrufbar"} | |
| if att.get("data") is None: | |
| return {**out, "hinweis": "Datei größer als 10 MB, nicht abgerufen"} | |
| if ext not in _EXTRACT: | |
| out["hinweis"] = NOT_READABLE | |
| return _unchecked(out, att, fid, release) if files else out | |
| try: | |
| text = _tidy(_EXTRACT[ext](att["data"])) | |
| except Exception: # corrupt/encrypted file; the message may quote its content | |
| text = "" | |
| if not text: | |
| out["hinweis"] = NOT_READABLE | |
| return _unchecked(out, att, fid, release) if files else out | |
| filtered = None | |
| if files: | |
| datei, filtered = _file_out(ext, att["data"], text, stats) | |
| if datei: | |
| out["datei"] = datei | |
| else: | |
| _unchecked(out, att, fid, release) | |
| shown = filtered if filtered is not None else text | |
| if len(shown) > MAX_TEXT: # cut at a space so no name is split in half | |
| shown, out["gekuerzt"] = shown[:MAX_TEXT].rsplit(None, 1)[0], True | |
| return {**out, "text": shown if filtered is not None else filter_text(shown, stats)} | |
| def filter_task(task, stats: Counter | None = None, files=False, release=()): | |
| """Whitelist: anything not named here (creator, groups, feedback, ...) cannot appear in the output.""" | |
| return { | |
| "id": int(task["id"]), | |
| "fach": filter_text(task.get("tags", ""), stats), | |
| "titel": filter_text(task.get("title", ""), stats), | |
| "beschreibung": filter_text(task.get("description", ""), stats), | |
| "start": task.get("start"), | |
| "abgabe": task.get("due"), | |
| "status": None if "done" not in task else "erledigt" if task["done"] else "offen", # None: source cannot tell | |
| "anhaenge": [describe_attachment(a, stats, files, f"{int(task['id'])}-{i}", release) for i, a in enumerate(task.get("attachments", []))], | |
| } | |