blob: fe88dd1927018320ac15d30a8fa4f37029735d4f [file] [log] [blame]
#!/usr/bin/env python3
"""Download proprietary blobs for Fairphone devices.
This tool helps to download and automatically extract device needed blobs in the
correct path into source tree.
"""
import argparse
from pathlib import Path
import logging
import sys
import subprocess
import shutil
import tarfile
import tempfile
import urllib.request
import urllib.error
BLOBS_BASE_URL = (
"https://storage.googleapis.com/fairphone-updates/"
"14812f31-efc8-4f57-a419-e14bb878e6dc/"
)
__log__ = logging.getLogger(__name__)
__sh__ = logging.StreamHandler()
__sh__.setFormatter(logging.Formatter("%(levelname)-6s - %(message)s"))
__log__.addHandler(__sh__)
class GetBlobsError(Exception):
"""Generic error that leads to aborting of this script."""
def parse_cmdline_arguments() -> argparse.Namespace:
"""Parse the command line arguments.
Returns:
argparse Namespace containing the parsed command line arguments of this
script.
"""
parser = argparse.ArgumentParser(
description=(
"Download and extract required proprietary blobs in the source "
"tree."
)
)
parser.add_argument(
"--device",
required=True,
help="Name of the device to download blobs for.",
)
parser.add_argument(
"--build-id",
required=True,
help="ID of the build to download blobs for.",
)
parser.add_argument(
"--blobs-dir",
type=Path,
required=True,
help="Directory to store the blobs in.",
)
parser.add_argument(
"--quiet", action="store_true", help="Print only fatal errors."
)
return parser.parse_args()
def progress_bar(count: int, total: int, status: str = "") -> None:
"""Show a simple progress bar on command line.
Arguments:
count: The current counter.
total: The expected total to complete.
status: String that is displayed on the left of the progress bar.
"""
bar_len = 60
filled_len = int(round(bar_len * count / float(total)))
percents = round(100.0 * count / float(total), 1)
_bar = "=" * filled_len + "-" * (bar_len - filled_len)
sys.stdout.write("[%s] %s%s ... %s\r" % (_bar, percents, "%", status))
sys.stdout.flush()
def download_with_progress(url: str, target_file_name: Path) -> None:
"""Download a file from a URL.
Arguments:
url: URL to download the file from.
target_file_name: Path object pointing to target file.
Throws:
GetBlobsError: In case of errors, after logging the reason of the error.
"""
try:
with urllib.request.urlopen(url) as response, open(
target_file_name, "wb"
) as out_file:
file_size = int( # type: ignore
response.getheader("content-length") # type: ignore
)
chunk_size = int(max(4096, file_size / 100))
downloaded_data = 0
while downloaded_data < file_size:
data = response.read(chunk_size)
out_file.write(data)
downloaded_data += len(data)
progress_bar(
downloaded_data,
file_size,
"[ {} MB / {} MB ]".format(
round((downloaded_data / 1024) / 1024, 2),
round((file_size / 1024) / 1024, 2),
),
)
print("") # To properly begin a new line
__log__.info("Download completed.")
except urllib.error.HTTPError as e:
__log__.error("HTTP response: %d %s", e.code, e.reason)
raise GetBlobsError()
except urllib.error.URLError:
__log__.error("Malformed download URL")
raise GetBlobsError()
def extract_archive(
archive_file_name: str,
script_file_name: str,
target_path: Path,
tmp_path: Path,
) -> None:
"""Extract a self-extracting archive into a target folder.
Arguments:
archive_file_name: File name of the archive to extract.
script_file_name:
File name of the self-extracting script inside the archive.
target_path: Destination for the extracted files.
tmp_path:
Temporary directory used during the extraction process. This must
exist and should be removed after calling this function.
Throws:
GetBlobsError: In case of errors, after logging the reason of the error.
"""
__log__.info("Extracting the archive...")
try:
archive = tarfile.open(archive_file_name)
archive.extractall(tmp_path)
except (tarfile.ReadError, tarfile.CompressionError):
__log__.error("Archive file is corrupted.")
raise GetBlobsError()
except tarfile.ExtractError:
__log__.error("Extraction error.")
raise GetBlobsError()
__log__.info("Extraction complete.")
# Executing makeself archive
try:
subprocess.run(
"{} --noexec --target {}".format(script_file_name, target_path),
shell=True,
)
except OSError:
__log__.error("Cannot execute self-extracting archive.")
raise GetBlobsError()
def main() -> None:
"""Entry point into the configure_lava command line tool."""
args = parse_cmdline_arguments()
if args.quiet:
__log__.setLevel(logging.ERROR)
else:
__log__.setLevel(logging.INFO)
base_name = args.device + "-" + args.build_id + "-blobs"
archive_name = base_name + ".tgz"
tmp_path = Path(tempfile.mkdtemp())
archive_file_name = tmp_path / archive_name
extracted_successfully = False
try:
# Download archive in temporary directory
__log__.info("Downloading %s...", archive_name)
download_with_progress(
url=BLOBS_BASE_URL + archive_name,
target_file_name=archive_file_name,
)
# Extract the self-extracting archive
extract_archive(
archive_file_name=archive_file_name,
script_file_name="{}.sh".format(tmp_path / base_name),
target_path=args.blobs_dir,
tmp_path=tmp_path,
)
extracted_successfully = True
finally:
# Clean the temporary directory
try:
shutil.rmtree(tmp_path)
except OSError:
__log__.warning("Cannot remove temporary folder.")
if extracted_successfully:
__log__.info("Done.")
if __name__ == "__main__":
main()