downloader.py 4.6 KB

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