downloader.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import asyncio
  2. import os
  3. from typing import Optional
  4. import aiofiles
  5. import aiohttp
  6. class DownloadJob:
  7. """
  8. Download Job
  9. :param file_url: url where the file is located
  10. :param session: aiohttp session to be used on the job
  11. :param file_name: name to be used on the file. Defaults to the last part of the url
  12. :param save_path: dir where the file should be saved. Defaults to the current dir
  13. """
  14. def __init__(
  15. self,
  16. session: aiohttp.ClientSession,
  17. file_url: str,
  18. save_path: Optional[str] = None,
  19. chunk_size: Optional[int] = 1024,
  20. ):
  21. self.file_url = file_url
  22. self._session = session
  23. self._chunk_size = chunk_size
  24. self.file_name = file_url.split("/")[~0]
  25. self.file_path = (
  26. os.path.join(save_path, self.file_name) if save_path else self.file_name
  27. )
  28. self.completed = False
  29. self.progress = 0
  30. self.size = 0
  31. async def get_size(self) -> int:
  32. """
  33. Gets the content-length of the file from the file url.
  34. :return: the files size in bytes
  35. """
  36. if not self.size:
  37. async with self._session.get(self.file_url) as resp:
  38. if 200 <= resp.status < 300:
  39. self.size = int(resp.headers["Content-Length"])
  40. else:
  41. raise aiohttp.errors.HttpProcessingError(
  42. message=f"There was a problem processing {self.file_url}",
  43. code=resp.status,
  44. )
  45. return self.size
  46. def _downloaded(self, chunk_size):
  47. """
  48. Method to be called when a chunk of the file is downloaded. It updates the
  49. progress, adds the size to the _advance variable and checks if the download
  50. is completed
  51. """
  52. self.progress += chunk_size
  53. async def download(self):
  54. """
  55. Downloads the file from the given url to a file in the given path.
  56. """
  57. async with self._session.get(self.file_url) as resp:
  58. # Checkning the response code
  59. if 200 <= resp.status < 300:
  60. # Saving the data to the file chunk by chunk.
  61. async with aiofiles.open(self.file_path, "wb") as file:
  62. # Downloading the file using the aiohttp.StreamReader
  63. async for data in resp.content.iter_chunked(self._chunk_size):
  64. await file.write(data)
  65. self._downloaded(self._chunk_size)
  66. self.completed = True
  67. return self
  68. else:
  69. raise aiohttp.errors.HttpProcessingError(
  70. message=f"There was a problem processing {self.file_url}",
  71. code=resp.status,
  72. )
  73. class Handler:
  74. """
  75. Top level interface with the downloader. It creates the download jobs and handles them.
  76. :param loop: asyncio loop. if not provided asyncio.get_event_loop() will be used
  77. :param session: aiohttp session to be used on all downloads. If not provided it will be
  78. created
  79. :param chunk_size: chunk bytes sizes to get from the file source. Defaults to 1024 bytes
  80. """
  81. def __init__(
  82. self,
  83. loop: Optional[asyncio.BaseEventLoop] = None,
  84. session: Optional[aiohttp.ClientSession] = None,
  85. chunk_size: Optional[int] = 1024,
  86. ):
  87. self._loop = loop or asyncio.get_event_loop()
  88. self._session = session or aiohttp.ClientSession(loop=self._loop)
  89. self._chunk_size = chunk_size
  90. def _job_factory(
  91. self, file_url: str, save_path: Optional[str] = None
  92. ) -> DownloadJob:
  93. """
  94. Shortcut for creating a download job. It adds the session and the chunk size.
  95. :param file_url: url where the file is located
  96. :param save_path: save path for the download
  97. :return:
  98. """
  99. return DownloadJob(self._session, file_url, save_path, self._chunk_size)
  100. async def download(self, url: str, save_path: Optional[str] = None) -> DownloadJob:
  101. """
  102. Downloads a bulk of files from the given list of urls to the given path.
  103. :param files_url: list of urls where the files are located
  104. :param save_path: path to be used for saving the files. Defaults to the current dir
  105. """
  106. job = self._job_factory(url, save_path=save_path)
  107. task = asyncio.ensure_future(job.download())
  108. await task
  109. file_name = url.split("/")[-1]
  110. path = os.getcwd() + "/" + file_name if not save_path else save_path
  111. return path