55import re
66import shutil
77import sys
8+ import urllib .error
89import urllib .request
910from datetime import datetime
1011
@@ -16,30 +17,63 @@ def check_version_format(ver):
1617 sys .exit ("Error: version must be full form like 25.08.0" )
1718 return ver
1819
19- def compute_md5_and_size (url ):
20- md5 = hashlib .md5 ()
21- size = 0
20+ def download_file (url ):
21+ """Download file from URL, yielding chunks.
22+
23+ Args:
24+ url: URL of the file to download
25+
26+ Yields:
27+ bytes: 1MB chunks of the file
28+
29+ Raises:
30+ urllib.error.HTTPError: If the URL returns an error (e.g., 404)
31+ urllib.error.URLError: If there's a network error
32+ """
2233 with urllib .request .urlopen (url ) as resp :
2334 while True :
2435 chunk = resp .read (1024 * 1024 )
2536 if not chunk :
2637 break
27- md5 .update (chunk )
28- size += len (chunk )
38+ yield chunk
39+
40+ def compute_md5_and_size (chunks ):
41+ """Compute MD5 hash and total size from data chunks.
42+
43+ Args:
44+ chunks: Iterable of byte chunks
45+
46+ Returns:
47+ tuple: (md5_hexdigest, size_in_bytes)
48+ """
49+ md5 = hashlib .md5 ()
50+ size = 0
51+ for chunk in chunks :
52+ md5 .update (chunk )
53+ size += len (chunk )
2954 return md5 .hexdigest (), size
3055
3156def main ():
32- parser = argparse .ArgumentParser (description = "Add a new Infix version to a GNS3 appliance file" )
57+ parser = argparse .ArgumentParser (
58+ description = "Add a new Infix version to a GNS3 appliance file" ,
59+ epilog = "The .gns3a appliance files are typically found in the appliances/ "
60+ "directory of the gns3-registry project."
61+ )
3362 parser .add_argument ("version" , help = "Infix version (e.g. 25.08.0)" )
34- parser .add_argument ("appliance" , help = "Path to appliance JSON (.gns3a)" )
63+ parser .add_argument ("appliance" , help = "Path to appliance JSON file (.gns3a)" )
3564 args = parser .parse_args ()
3665
3766 version = check_version_format (args .version )
3867 appliance_path = args .appliance
3968
4069 # Load JSON
41- with open (appliance_path , "r" , encoding = "utf-8" ) as f :
42- data = json .load (f )
70+ try :
71+ with open (appliance_path , "r" , encoding = "utf-8" ) as f :
72+ data = json .load (f )
73+ except FileNotFoundError :
74+ sys .exit (f"Error: Appliance file not found: { appliance_path } " )
75+ except json .JSONDecodeError as e :
76+ sys .exit (f"Error: Invalid JSON in appliance file: { e } " )
4377
4478 # Skip if version already exists
4579 for v in data .get ("versions" , []):
@@ -52,7 +86,18 @@ def main():
5286 url = f"{ REPO } /releases/download/v{ version } /{ filename } "
5387
5488 print (f"Downloading { url } to compute MD5 and size..." )
55- md5sum , size = compute_md5_and_size (url )
89+ try :
90+ chunks = download_file (url )
91+ md5sum , size = compute_md5_and_size (chunks )
92+ except urllib .error .HTTPError as e :
93+ if e .code == 404 :
94+ sys .exit (f"Error: Version { version } not found. "
95+ f"The release v{ version } does not exist or the disk image is not available.\n "
96+ f"URL: { url } " )
97+ else :
98+ sys .exit (f"Error: HTTP { e .code } when downloading: { e .reason } \n URL: { url } " )
99+ except urllib .error .URLError as e :
100+ sys .exit (f"Error: Network error while downloading: { e .reason } \n URL: { url } " )
56101
57102 # Add image entry
58103 image_entry = {
0 commit comments