package_downloader.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. #
  2. # Copyright (c) Contributors to the Open 3D Engine Project.
  3. # For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. #
  8. import os
  9. import urllib
  10. import urllib.request
  11. from urllib.error import URLError
  12. import ssl
  13. import certifi
  14. import hashlib
  15. import pathlib
  16. import tarfile
  17. import sys
  18. # used if LY_PACKAGE_SERVER_URLS is not set.
  19. DEFAULT_LY_PACKAGE_SERVER_URLS = "https://d2c171ws20a1rv.cloudfront.net"
  20. # its not necessarily the case that you ever actually have to use boto3
  21. # if all the servers you specify in your server list (Default above) are
  22. # not s3 buckets. So it is not a failure to be missing boto3 unless you actually
  23. # try to use it later.
  24. _aws_s3_available = False
  25. try:
  26. import boto3
  27. from botocore.exceptions import BotoCoreError
  28. from botocore.exceptions import ClientError
  29. _aws_s3_available = True
  30. except:
  31. pass
  32. class PackageDownloader():
  33. @staticmethod
  34. def DownloadAndUnpackPackage(package_name, package_hash, folder_target):
  35. '''Given a package name, hash, and folder to unzip it into,
  36. attempts to find the package. If found, downloads and unpacks to the target_folder location.
  37. Only the first found package is downloaded, and then the search stops. If the checksum of
  38. the downloaded file doesn't match the checksum in the O3DE dependency list, the package
  39. isn't unpacked on the filesystem and the download is deleted.
  40. This method supports all URI types handled by the O3DE package system, including S3 URIs.
  41. PRECONDITIONS:
  42. * LY_PACKAGE_SERVER_URLS must be set in the environment to override the defaultg
  43. * If using S3 URIs, LY_AWS_PROFILE must be set in the environment and the 'aws' command
  44. must be on the PATH
  45. Returns True if successful, False otherwise.
  46. '''
  47. def ComputeHashOfFile(file_path):
  48. file_path = os.path.normpath(file_path)
  49. hasher = hashlib.sha256()
  50. hash_result = None
  51. # we don't follow symlinks here, this is strictly to check actual packages.
  52. with open(file_path, 'rb') as afile:
  53. buf = afile.read()
  54. hasher.update(buf)
  55. hash_result = hasher.hexdigest()
  56. return hash_result
  57. # make sure a package with that name is not already present:
  58. server_urls = os.environ.get("LY_PACKAGE_SERVER_URLS", default = "")
  59. if not server_urls:
  60. print(f"Server url list is empty - please set LY_PACKAGE_SERVER_URLS env var to semicolon-seperated list of urls to check")
  61. print(f"Using default URL for convenience: {DEFAULT_LY_PACKAGE_SERVER_URLS}")
  62. server_urls = DEFAULT_LY_PACKAGE_SERVER_URLS
  63. download_location = pathlib.Path(folder_target)
  64. package_file_name = package_name + ".tar.xz"
  65. package_download_name = download_location / package_file_name
  66. package_unpack_folder = download_location / package_name
  67. server_list = server_urls.split(';')
  68. try:
  69. package_download_name.unlink()
  70. except FileNotFoundError:
  71. pass
  72. download_location.mkdir(parents=True, exist_ok=True)
  73. print(f"Downloading package {package_name}...")
  74. for package_server in server_list:
  75. if not package_server:
  76. continue
  77. full_package_url = package_server + "/" + package_file_name
  78. print(f" - attempting '{full_package_url}' ...")
  79. try:
  80. if full_package_url.startswith("s3://"):
  81. if not _aws_s3_available:
  82. print(f"S3 URL given, but boto3 could not be located. Please ensure that you have installed")
  83. print(f"installed requirements: {sys.executable} -m pip install --upgrade boto3 certifi six")
  84. continue
  85. # it may be legitimate not have a blank AWS profile, so we can't supply a default here
  86. aws_profile_name = os.environ.get("LY_AWS_PROFILE", default = "")
  87. # if it is blank, its still worth noting in the log:
  88. if not aws_profile_name:
  89. print(" - LY_AWS_PROFILE env var is not set - using blank AWS profile by default")
  90. session = boto3.session.Session(profile_name=aws_profile_name)
  91. bucket_name = full_package_url[len("s3://"):]
  92. slash_pos = bucket_name.find('/')
  93. if slash_pos != -1:
  94. bucket_name = bucket_name[:slash_pos]
  95. print(f" - using aws to download {package_file_name} from bucket {bucket_name}...")
  96. session.client('s3').download_file(bucket_name, package_file_name, str(package_download_name))
  97. else:
  98. tls_context = ssl.create_default_context(cafile=certifi.where())
  99. with urllib.request.urlopen(url=full_package_url, context = tls_context) as server_response:
  100. print(" - Downloading package...")
  101. file_data = server_response.read()
  102. with open(package_download_name, "wb") as save_package:
  103. save_package.write(file_data)
  104. except (ClientError, BotoCoreError, ssl.SSLError, URLError, OSError) as e:
  105. print(f" - Unable to get package from this server: {e}")
  106. continue # try the next URL, if any...
  107. try:
  108. # validate that the package matches its hash
  109. print(" - Checking hash ... ")
  110. hash_result = ComputeHashOfFile(str(package_download_name))
  111. if hash_result != package_hash:
  112. print(" - Warning: Hash of package does not match - will not use it")
  113. continue
  114. # hash matched. Unpack and return!
  115. package_unpack_folder.mkdir(parents=True, exist_ok=True)
  116. with tarfile.open(package_download_name) as archive_file:
  117. print(" - unpacking package...")
  118. archive_file.extractall(package_unpack_folder)
  119. print(f"Downloaded successfuly to {os.path.realpath(package_unpack_folder)}")
  120. return True
  121. except (OSError, tarfile.TarError) as e:
  122. # note that anything that causes this to fail should result in trying the next one.
  123. print(f" - unable to unpack or verify the package: {e}")
  124. continue # try the next server, if you have any
  125. finally:
  126. # clean up
  127. if os.path.exists(package_download_name):
  128. try:
  129. os.remove(package_download_name)
  130. except:
  131. pass
  132. print("FAILED - unable to find the package on any servers.")
  133. return False
  134. # you can also use this module from a bash script to get a package
  135. if __name__ == '__main__':
  136. import argparse
  137. parser = argparse.ArgumentParser(description="Download, verify hash, and unpack a 3p package")
  138. parser.add_argument('--package-name',
  139. help='The package name to download',
  140. required=True)
  141. parser.add_argument('--package-hash',
  142. help='The package hash to verify',
  143. required=True)
  144. parser.add_argument('--output-folder',
  145. help='The folder to unpack to. It will get unpacked into (package-name) subfolder!',
  146. required=True)
  147. parsed_args = parser.parse_args()
  148. if PackageDownloader.DownloadAndUnpackPackage(parsed_args.package_name, parsed_args.package_hash, parsed_args.output_folder):
  149. sys.exit(0)
  150. sys.exit(1)