diff --git a/src/dpy_cache/dpy_cache.py b/src/dpy_cache/dpy_cache.py index 928cd9f..9b3ba99 100644 --- a/src/dpy_cache/dpy_cache.py +++ b/src/dpy_cache/dpy_cache.py @@ -122,10 +122,13 @@ class DPYCache: ) -> CacheResult: """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, - DPYCacheError on a duplicate filename in a list input (checked before any upload, so - a collision costs zero requests) or an attachment-count mismatch after a send, and - propagates discord's own Forbidden/HTTPException unwrapped on a send failure""" + DPYCacheError on a duplicate filename in a list input, an unsupported content type, + or a missing file path (all checked before any upload, so a locally-detectable bad + 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) + self._prevalidate(items) merged: dict[str, FileRef] = {} jump_urls: list[str] = [] @@ -248,6 +251,27 @@ class DPYCache: items.append((name, source)) 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: """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