downloader.py 4.6 KB

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