123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- #
- # Copyright (c) Contributors to the Open 3D Engine Project.
- # For complete copyright and license terms please see the LICENSE at the root of this distribution.
- #
- # SPDX-License-Identifier: Apache-2.0 OR MIT
- #
- #
- import os
- import urllib
- import urllib.request
- from urllib.error import URLError
- import ssl
- import certifi
- import hashlib
- import pathlib
- import tarfile
- import sys
- from urllib.parse import _splithost
- # used if LY_PACKAGE_SERVER_URLS is not set.
- DEFAULT_LY_PACKAGE_SERVER_URLS = "https://d2c171ws20a1rv.cloudfront.net;https://d3t6xeg4fgfoum.cloudfront.net"
- possible_download_errors = (ssl.SSLError, URLError, OSError)
- # its not necessarily the case that you ever actually have to use boto3
- # if all the servers you specify in your server list (Default above) are
- # not s3 buckets. So it is not a failure to be missing boto3 unless you actually
- # try to use it later.
- _aws_s3_available = False
- try:
- import boto3
- from botocore.exceptions import BotoCoreError
- from botocore.exceptions import ClientError
- _aws_s3_available = True
- possible_download_errors = possible_download_errors + (ClientError, BotoCoreError)
- except:
- print("Could not import boto3 (pip install boto3) - downloading from S3 buckets will not function")
- pass
- class PackageDownloader():
- @staticmethod
- def ComputeHashOfFile(file_path):
- '''
- Compute a sha256 hex-encoded hash for the contents of a file represented by file_path
- '''
- file_path = os.path.normpath(file_path)
- hasher = hashlib.sha256()
- hash_result = None
-
- # we don't follow symlinks here, this is strictly to check actual packages.
- with open(file_path, 'rb') as afile:
- buf = afile.read()
- hasher.update(buf)
- hash_result = hasher.hexdigest()
-
- return hash_result
- def ValidateUnpackedPackage(package_name, package_hash, folder_target):
- '''
- This function will determine the integrity of a download and unpacked package.
- Given a package name, hash, and folder where a package was previously unpacked,
- this will verify the package's SHA256SUMS integrity file against the files in the
- folder. In there are any files missing or corrupted, then the function will return
- False, otherwise it will return True.
- '''
- download_location = pathlib.Path(folder_target)
- package_unpack_folder = download_location / package_name
- if not package_unpack_folder.is_dir():
- return False;
- sha256_sums_file_path = package_unpack_folder / 'SHA256SUMS'
- if not sha256_sums_file_path.is_file():
- return False;
- with sha256_sums_file_path.open() as sha256_sums_file:
- sha256_sums = sha256_sums_file.readlines()
- for sha256_sum_line in sha256_sums:
- sha256_sum, src_file = sha256_sum_line.split(' *')
- src_file_full_path = package_unpack_folder / src_file.strip()
- if not src_file_full_path.is_file():
- return False
- computed_hash = PackageDownloader.ComputeHashOfFile(str(src_file_full_path))
- if computed_hash != sha256_sum:
- print(f"Existing package {package_name} not valid ({src_file} sum doesnt match)")
- return False
- return True
- @staticmethod
- def DownloadAndUnpackPackage(package_name, package_hash, folder_target):
- '''Given a package name, hash, and folder to unzip it into,
- attempts to find the package. If found, downloads and unpacks to the target_folder location.
- Only the first found package is downloaded, and then the search stops. If the checksum of
- the downloaded file doesn't match the checksum in the O3DE dependency list, the package
- isn't unpacked on the filesystem and the download is deleted.
-
- This method supports all URI types handled by the O3DE package system, including S3 URIs.
-
- PRECONDITIONS:
- * LY_PACKAGE_SERVER_URLS must be set in the environment to override the defaultg
- * If using S3 URIs, LY_AWS_PROFILE must be set in the environment and the 'aws' command
- must be on the PATH
-
- Returns True if successful, False otherwise.
- '''
- # make sure a package with that name is not already present:
- server_urls = os.environ.get("LY_PACKAGE_SERVER_URLS", default = "")
- if not server_urls:
- print(f"Server url list is empty - please set LY_PACKAGE_SERVER_URLS env var to semicolon-seperated list of urls to check")
- print(f"Using default URL for convenience: {DEFAULT_LY_PACKAGE_SERVER_URLS}")
- server_urls = DEFAULT_LY_PACKAGE_SERVER_URLS
- download_location = pathlib.Path(folder_target)
- package_file_name = package_name + ".tar.xz"
- package_download_name = download_location / package_file_name
- package_unpack_folder = download_location / package_name
-
- server_list = server_urls.split(';')
- try:
- package_download_name.unlink()
- except FileNotFoundError:
- pass
- download_location.mkdir(parents=True, exist_ok=True)
- print(f"Downloading package {package_name}...")
- for package_server in server_list:
- if not package_server:
- continue
- full_package_url = package_server + "/" + package_file_name
- try:
- # check if its a local file (gets around an issue with parsing urls in py3.10.x)
- parse_result = urllib.parse.urlparse(full_package_url)
- if parse_result.scheme == 'file':
- actual_path = ""
- if parse_result.netloc:
- actual_path = urllib.request.url2pathname(parse_result.netloc + parse_result.path)
- else:
- actual_path = urllib.request.url2pathname(parse_result.path)
- # 'download' a local file:
- file_data = None
- print(f" - Reading from local file: {actual_path}")
- with open(actual_path, "rb") as input_file:
- file_data = input_file.read()
- with open(package_download_name, "wb") as save_package:
- save_package.write(file_data)
- elif full_package_url.startswith("s3://"):
- if not _aws_s3_available:
- print(f"S3 URL given, but boto3 could not be located. Please ensure that you have installed")
- print(f"installed requirements: {sys.executable} -m pip install --upgrade boto3 certifi six")
- continue
- # it may be legitimate not have a blank AWS profile, so we can't supply a default here
- aws_profile_name = os.environ.get("LY_AWS_PROFILE", default = "")
- # if it is blank, its still worth noting in the log:
- if not aws_profile_name:
- print(" - LY_AWS_PROFILE env var is not set - using blank AWS profile by default")
- session = boto3.session.Session(profile_name=aws_profile_name)
- bucket_name = full_package_url[len("s3://"):]
- slash_pos = bucket_name.find('/')
- if slash_pos != -1:
- bucket_name = bucket_name[:slash_pos]
- print(f" - using aws to download {package_file_name} from bucket {bucket_name}...")
- session.client('s3').download_file(bucket_name, package_file_name, str(package_download_name))
- else:
- tls_context = ssl.create_default_context(cafile=certifi.where())
- print(f" - Trying URL: {full_package_url}")
- with urllib.request.urlopen(url=full_package_url, context = tls_context) as server_response:
-
- file_data = server_response.read()
- with open(package_download_name, "wb") as save_package:
- save_package.write(file_data)
- except possible_download_errors as e:
- print(f" - Unable to get package from this server: {e}")
- continue # try the next URL, if any...
- try:
- # validate that the package matches its hash
- print(" - Checking hash ... ")
- hash_result = PackageDownloader.ComputeHashOfFile(str(package_download_name))
- if hash_result != package_hash:
- print(" - Warning: Hash of package does not match - will not use it")
- continue
- # hash matched. Unpack and return!
- package_unpack_folder.mkdir(parents=True, exist_ok=True)
- with tarfile.open(package_download_name) as archive_file:
- print(" - unpacking package...")
- archive_file.extractall(package_unpack_folder)
- print(f"Downloaded successfuly to {os.path.realpath(package_unpack_folder)}")
- return True
- except (OSError, tarfile.TarError) as e:
- # note that anything that causes this to fail should result in trying the next one.
- print(f" - unable to unpack or verify the package: {e}")
- continue # try the next server, if you have any
- finally:
- # clean up
- if os.path.exists(package_download_name):
- try:
- os.remove(package_download_name)
- except:
- pass
- print("FAILED - unable to find the package on any servers.")
- return False
- # you can also use this module from a bash script to get a package
- if __name__ == '__main__':
- import argparse
- parser = argparse.ArgumentParser(description="Download, verify hash, and unpack a 3p package")
- parser.add_argument('--package-name',
- help='The package name to download',
- required=True)
- parser.add_argument('--package-hash',
- help='The package hash to verify',
- required=True)
-
- parser.add_argument('--output-folder',
- help='The folder to unpack to. It will get unpacked into (package-name) subfolder!',
- required=True)
- parsed_args = parser.parse_args()
- if PackageDownloader.DownloadAndUnpackPackage(parsed_args.package_name, parsed_args.package_hash, parsed_args.output_folder):
- sys.exit(0)
- sys.exit(1)
|