diff --git a/examples/client_example/1.root.json b/examples/client_example/1.root.json new file mode 100644 index 00000000..214d8db0 Binary files /dev/null and b/examples/client_example/1.root.json differ diff --git a/examples/client_example/README.rst b/examples/client_example/README.rst new file mode 100644 index 00000000..5fc1c815 --- /dev/null +++ b/examples/client_example/README.rst @@ -0,0 +1,92 @@ +Python Client Example +##################### + +Introduction +============ + +Python Client Example, using ``python-tuf``. + +For information about installing ``python-tuf``, please refer to the +`Installation documentation `_. + + +Preparing +========= + +To have the example working in your machine, clone the ``python-tuf`` in your +system. + +.. code:: console + + $ git clone git@github.com:theupdateframework/python-tuf.git + + +Repository +========== + +As this example demonstrates how to use the ``python-tuf`` to build a +client application, the repository will use static files. + +The static files are available in the ``python-tuf`` repository, same as this. +The static repository files are in +``tests/repository_data/repository``. + +Run the repository using the Python3 built-in HTTP module, and keep this +session running. + +.. code:: console + + $ python3 -m http.server -d tests/repository_data/repository + Serving HTTP on :: port 8000 (http://[::]:8000/) ... + + +Client Example +============== + +The `source code is available entirely <./client_example.py>`_ in this +repository. + +How to use the Client Example: + +1. Initialize the Client + + .. code:: console + + $ ./client_example.py --init + + + This action is to create the client infrastructure properly. + + This infrastructure consists in: + - Metadata repository + - Download folder for targets + - Bootstrap 1.root.json + + +2. Download the ``file1.txt`` + + .. code:: console + + $ ./client_example.py download file1.txt + [INFO] Top-level metadata is refreshed. + [INFO] Target info gotten. + [INFO] File downloaded available in ./downloads/file2.txt. + + +3. Download a not available ``file_na.txt`` + + .. code:: console + + $ ./client_example.py download file_na.txt + [INFO] Top-level metadata is refreshed. + [INFO] Target info gotten. + [ERROR] Target file not found. + +4. Download again ``file1.txt`` + + .. code:: console + + $ ./client_example.py download file1.txt + [INFO] Top-level metadata is refreshed. + [INFO] Target info gotten. + [INFO] File is already available in ./downloads/file1.txt. diff --git a/examples/client_example/client_example.py b/examples/client_example/client_example.py new file mode 100755 index 00000000..2a59b853 --- /dev/null +++ b/examples/client_example/client_example.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +import argparse +import os +import shutil +import sys +from logging import exception +from pathlib import Path + +from requests.exceptions import ConnectionError + +from tuf.ngclient import Updater + +# define directory constants +HOME_DIR = Path.home() # user home dir +DOWNLOAD_DIR = "./downloads" # download dir +METADATA_DIR = f"{HOME_DIR}/.local/share/tuf_metadata_example" # metadata dir +CLIENT_EXAMPLE_DIR = os.path.dirname(os.path.abspath(__file__)) # example dir + + +def init(): + """ + Initialize the TUF Client infrastructure + + This function initializes the creation of the download and TUF metadata + directory. + """ + + if not os.path.isdir(DOWNLOAD_DIR): + os.mkdir(DOWNLOAD_DIR) + + print(f"[INFO] Download directory [{DOWNLOAD_DIR}] is created.") + + if not os.path.isdir(METADATA_DIR): + os.makedirs(METADATA_DIR) + + print(f"[INFO] Metadata folder [{METADATA_DIR}] is created.") + + if not os.path.isfile(f"{METADATA_DIR}/root.json"): + shutil.copy( + f"{CLIENT_EXAMPLE_DIR}/1.root.json", f"{METADATA_DIR}/root.json" + ) + print(f"[INFO] Bootstrap initial root metadata.") + + +def tuf_updater(): + """ + This function implement the ``tuf.ngclient.Updater`` and returns + the updater. + """ + url = "http://127.0.0.1:8000" + + try: + updater = Updater( + repository_dir=METADATA_DIR, + metadata_base_url=f"{url}/metadata/", + target_base_url=f"{url}/targets/", + target_dir=DOWNLOAD_DIR, + ) + + except FileNotFoundError: + print("[ERROR] The Example Client not initiated. Try using '--init'.") + sys.exit(1) + + return updater + + +def download(target): + """ + Download the target file using the TUF ``nglcient`` Updater process. + + The Updater refreshes the top-level metadata, get the target information, + verifies if the target is already cached, and in case it is not cached, + downloads the target file. + """ + + try: + updater = tuf_updater() + + except ConnectionError: + print("[ERROR] Failed to connect http://127.0.0.1:8000") + sys.exit(1) + + updater.refresh() + print("[INFO] Top-level metadata is refreshed.") + + info = updater.get_targetinfo(target) + print("[INFO] Target info gotten.") + + if info is None: + print("[ERROR] Target file not found.") + sys.exit(1) + + path = updater.find_cached_target(info) + if path: + print( + f"[INFO] File is already available in {DOWNLOAD_DIR}/{info.path}." + ) + sys.exit(0) + + path = updater.download_target(info) + + print(f"[INFO] File downloaded available in {DOWNLOAD_DIR}/{info.path}.") + + +if __name__ == "__main__": + + client_args = argparse.ArgumentParser( + description="TUF Python Client Example" + ) + + # Global arguments + client_args.add_argument( + "--init", + default=False, + help="Force register a new Engine.", + action="store_true", + ) + + # Sub commands + sub_commands = client_args.add_subparsers(dest="sub_commands") + + # Download + download_parser = sub_commands.add_parser( + "download", + help="Download a target file", + ) + + download_parser.add_argument( + "target", + metavar="TARGET", + help="Target file", + ) + + command_args = vars(client_args.parse_args()) + sub_commands_args = command_args.get("sub_commands") + + if command_args.get("init") is True: + init() + + elif not sub_commands_args: + client_args.print_help() + + if sub_commands_args == "download": + target = command_args.get("target") + download(target)