Skip to content

Commit 54f6576

Browse files
hzhrealCopilotplun1331pre-commit-ci[bot]Paillat-dev
authored
feat: make attachment downloading chunkable (#2956)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: plun1331 <plun1331@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Paillat <jeremiecotti@ik.me> Co-authored-by: Lala Sabathil <lala@pycord.dev> Co-authored-by: Paillat <paillat@pycord.dev> Co-authored-by: Lala Sabathil <aiko@aitsys.dev>
1 parent fc0b623 commit 54f6576

File tree

3 files changed

+91
-4
lines changed

3 files changed

+91
-4
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ These changes are available on the `master` branch, but have not yet been releas
1414

1515
### Added
1616

17+
- Added `Attachment.read_chunked` and added optional `chunksize` argument to
18+
`Attachment.save` for retrieving attachments in chunks.
19+
([#2956](https://github.com/Pycord-Development/pycord/pull/2956))
20+
1721
### Changed
1822

1923
### Fixed

discord/http.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,15 @@
2929
import logging
3030
import sys
3131
import weakref
32-
from typing import TYPE_CHECKING, Any, Coroutine, Iterable, Sequence, TypeVar
32+
from typing import (
33+
TYPE_CHECKING,
34+
Any,
35+
AsyncGenerator,
36+
Coroutine,
37+
Iterable,
38+
Sequence,
39+
TypeVar,
40+
)
3341
from urllib.parse import quote as _uriquote
3442

3543
import aiohttp
@@ -406,6 +414,21 @@ async def get_from_cdn(self, url: str) -> bytes:
406414
else:
407415
raise HTTPException(resp, "failed to get asset")
408416

417+
async def stream_from_cdn(self, url: str, chunksize: int) -> AsyncGenerator[bytes]:
418+
if not isinstance(chunksize, int) or chunksize < 1:
419+
raise InvalidArgument("The chunksize must be a positive integer.")
420+
421+
async with self.__session.get(url) as resp:
422+
if resp.status == 200:
423+
async for chunk in resp.content.iter_chunked(chunksize):
424+
yield chunk
425+
elif resp.status == 404:
426+
raise NotFound(resp, "asset not found")
427+
elif resp.status == 403:
428+
raise Forbidden(resp, "cannot retrieve asset")
429+
else:
430+
raise HTTPException(resp, "failed to get asset")
431+
409432
# state management
410433

411434
async def close(self) -> None:

discord/message.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from typing import (
3333
TYPE_CHECKING,
3434
Any,
35+
AsyncGenerator,
3536
Callable,
3637
ClassVar,
3738
Sequence,
@@ -290,6 +291,7 @@ async def save(
290291
*,
291292
seek_begin: bool = True,
292293
use_cached: bool = False,
294+
chunksize: int | None = None,
293295
) -> int:
294296
"""|coro|
295297
@@ -311,6 +313,8 @@ async def save(
311313
after the message is deleted. Note that this can still fail to download
312314
deleted attachments if too much time has passed, and it does not work
313315
on some types of attachments.
316+
chunksize: Optional[:class:`int`]
317+
The maximum size of each chunk to process.
314318
315319
Returns
316320
-------
@@ -323,16 +327,33 @@ async def save(
323327
Saving the attachment failed.
324328
NotFound
325329
The attachment was deleted.
330+
InvalidArgument
331+
Argument `chunksize` is less than 1.
326332
"""
327-
data = await self.read(use_cached=use_cached)
333+
if chunksize is not None:
334+
data = self.read_chunked(use_cached=use_cached, chunksize=chunksize)
335+
else:
336+
data = await self.read(use_cached=use_cached)
337+
328338
if isinstance(fp, io.BufferedIOBase):
329-
written = fp.write(data)
339+
if chunksize:
340+
written = 0
341+
async for chunk in data:
342+
written += fp.write(chunk)
343+
else:
344+
written = fp.write(data)
330345
if seek_begin:
331346
fp.seek(0)
332347
return written
333348
else:
334349
with open(fp, "wb") as f:
335-
return f.write(data)
350+
if chunksize:
351+
written = 0
352+
async for chunk in data:
353+
written += f.write(chunk)
354+
return written
355+
else:
356+
return f.write(data)
336357

337358
async def read(self, *, use_cached: bool = False) -> bytes:
338359
"""|coro|
@@ -369,6 +390,45 @@ async def read(self, *, use_cached: bool = False) -> bytes:
369390
data = await self._http.get_from_cdn(url)
370391
return data
371392

393+
async def read_chunked(
394+
self, chunksize: int, *, use_cached: bool = False
395+
) -> AsyncGenerator[bytes]:
396+
"""|coro|
397+
398+
Retrieves the content of this attachment in chunks as a :class:`AsyncGenerator` object of bytes.
399+
400+
Parameters
401+
----------
402+
chunksize: :class:`int`
403+
The maximum size of each chunk to process.
404+
use_cached: :class:`bool`
405+
Whether to use :attr:`proxy_url` rather than :attr:`url` when downloading
406+
the attachment. This will allow attachments to be saved after deletion
407+
more often, compared to the regular URL which is generally deleted right
408+
after the message is deleted. Note that this can still fail to download
409+
deleted attachments if too much time has passed, and it does not work
410+
on some types of attachments.
411+
412+
Yields
413+
------
414+
:class:`bytes`
415+
A chunk of the file.
416+
417+
Raises
418+
------
419+
HTTPException
420+
Downloading the attachment failed.
421+
Forbidden
422+
You do not have permissions to access this attachment
423+
NotFound
424+
The attachment was deleted.
425+
InvalidArgument
426+
Argument `chunksize` is less than 1.
427+
"""
428+
url = self.proxy_url if use_cached else self.url
429+
async for chunk in self._http.stream_from_cdn(url, chunksize):
430+
yield chunk
431+
372432
async def to_file(self, *, use_cached: bool = False, spoiler: bool = False) -> File:
373433
"""|coro|
374434

0 commit comments

Comments
 (0)