filename.py 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. """Print-file filename validation matching Bambu Studio's save-dialog rules.
  2. The Bambu printer SD card is FAT32/exFAT. Names containing the Windows /
  3. DOS-reserved set (``< > : " / \\ | ? *``), ASCII control characters
  4. (0x00-0x1F), or trailing dots / spaces cannot be created on it — FTP fails
  5. with ``553 Could not create file`` (#1540). Bambu Studio refuses to save
  6. such names client-side; Bambuddy now does the same at the rename, upload,
  7. and dispatch boundaries so the failure surfaces with a clear message
  8. instead of an obscure FTP error after the user has already hit Print.
  9. """
  10. INVALID_FILENAME_CHARS = '<>:"/\\|?*'
  11. # FAT/exFAT cap on a single path component; UTF-8 byte length, not codepoints,
  12. # because that is what the on-disk encoding limit actually is.
  13. MAX_FILENAME_BYTES = 255
  14. class InvalidFilenameError(ValueError):
  15. """Filename contains characters or shape the printer SD card rejects.
  16. ``char`` is the first offending character when the failure is a
  17. character-set violation, or ``None`` for structural failures (empty,
  18. bare ``.``, trailing space, too long, etc.). The frontend echoes it
  19. back to the user in the Bambu Studio-style error message.
  20. """
  21. def __init__(self, message: str, char: str | None = None):
  22. super().__init__(message)
  23. self.char = char
  24. def validate_print_filename(name: str) -> None:
  25. """Raise ``InvalidFilenameError`` if ``name`` would fail on the SD card.
  26. Matches Bambu Studio's save-dialog rejection set. Callers are expected
  27. to translate the exception into an HTTP 400 (or a clean dispatch
  28. rejection); the message is intentionally short and ASCII so it fits
  29. a translation template.
  30. """
  31. if not name or not name.strip():
  32. raise InvalidFilenameError("Filename cannot be empty")
  33. if name in (".", ".."):
  34. raise InvalidFilenameError("Filename cannot be '.' or '..'")
  35. for ch in name:
  36. if ch in INVALID_FILENAME_CHARS:
  37. raise InvalidFilenameError(f"Filename contains invalid character: {ch}", char=ch)
  38. if ord(ch) < 0x20:
  39. raise InvalidFilenameError("Filename contains a control character", char=ch)
  40. if name.endswith(" ") or name.endswith("."):
  41. raise InvalidFilenameError("Filename cannot end with a space or dot")
  42. if len(name.encode("utf-8")) > MAX_FILENAME_BYTES:
  43. raise InvalidFilenameError(f"Filename exceeds {MAX_FILENAME_BYTES} bytes")