fix: pre-validate all cache() inputs before any upload so a bad late input orphans nothing

cache() built discord.File objects per batch inside the send loop, so an unsupported type
or a missing file path in the 11th+ item raised only after earlier batches were already
uploaded - orphaning those stored messages with no returned ref. A new _prevalidate pass
checks every source up front (supported type; a path source exists and is a file) WITHOUT
opening any File or reading bytes, so a locally-detectable bad input fails loud before the
first send. The File is still built per batch in the loop (fresh-File-per-send unchanged),
so no extra open FDs are held. Neg control: old code uploaded batch 1 then raised on the
bad path (sends=1, orphaned); new raises with sends=0.

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
2026-07-06 20:02:19 -04:00
parent 96cf2e86ba
commit 51a4f7a817
+27 -3
View File
@@ -122,10 +122,13 @@ class DPYCache:
) -> CacheResult: ) -> CacheResult:
"""upload files to the storage channel (batching over 10/message) and return refs """upload files to the storage channel (batching over 10/message) and return refs
keyed by original filename; raises ValueError on unnamed bytes in a list input, keyed by original filename; raises ValueError on unnamed bytes in a list input,
DPYCacheError on a duplicate filename in a list input (checked before any upload, so DPYCacheError on a duplicate filename in a list input, an unsupported content type,
a collision costs zero requests) or an attachment-count mismatch after a send, and or a missing file path (all checked before any upload, so a locally-detectable bad
propagates discord's own Forbidden/HTTPException unwrapped on a send failure""" input in a >10-file batch costs zero requests and orphans nothing), or an
attachment-count mismatch after a send, and propagates discord's own
Forbidden/HTTPException unwrapped on a send failure"""
items = self._normalize(files) items = self._normalize(files)
self._prevalidate(items)
merged: dict[str, FileRef] = {} merged: dict[str, FileRef] = {}
jump_urls: list[str] = [] jump_urls: list[str] = []
@@ -248,6 +251,27 @@ class DPYCache:
items.append((name, source)) items.append((name, source))
return items return items
def _prevalidate(self, items: "list[tuple[str, FileContent]]") -> None:
"""check every source is a supported, locally-resolvable input BEFORE any upload
catches an unsupported content type or a missing/directory file path up front so a
bad input in a >10-file batch fails loud before the first send rather than after
earlier batches already landed (which would orphan those stored messages). does NOT
open Files or read bytes - only a cheap type check and, for a path source, an
existence/is-file probe; the actual File is still built per batch in the send loop.
"""
for name, source in items:
if isinstance(source, (discord.File, bytes, bytearray)):
continue
if isinstance(source, (str, os.PathLike)):
path = os.fspath(source)
if not os.path.isfile(path):
raise DPYCacheError(
f"file source for {name!r} is not an existing file: {path!r}"
)
continue
raise ValueError(f"unsupported file content type for {name!r}: {type(source).__name__}")
def _to_file(self, name: str, source: FileContent) -> discord.File: def _to_file(self, name: str, source: FileContent) -> discord.File:
"""build a genuinely fresh discord.File for ``source`` named ``name``, distinct from """build a genuinely fresh discord.File for ``source`` named ``name``, distinct from
any File previously handed to a send; a fresh File is built per send so the source any File previously handed to a send; a fresh File is built per send so the source