77from urllib .parse import urlparse
88from urllib .request import urlopen , Request
99
10- INDEX_PATH = os .getenv ("INDEX_PATH" , "ftp/downloads/__index_windows__.json" )
11- INDEX_URL = os .getenv ("INDEX_URL" ) or f"https://www.python.org/{ INDEX_PATH } "
10+ UPLOAD_URL_PREFIX = os .getenv ("UPLOAD_URL_PREFIX" ) or "https://www.python.org/ftp/"
11+ UPLOAD_PATH_PREFIX = os .getenv ("UPLOAD_PATH_PREFIX" ) or "/srv/www.python.org/ftp/"
12+ INDEX_URL = os .getenv ("INDEX_URL" ) or f"https://www.python.org/ftp/python/__index_windows__.json"
1213UPLOAD_HOST = os .getenv ("PyDotOrgServer" )
14+ UPLOAD_HOST_KEY = os .getenv ("PyDotOrgHostKey" )
15+ UPLOAD_KEYFILE = os .getenv ("UPLOAD_KEYFILE" )
16+ UPLOAD_USER = os .getenv ("PyDotOrgUsername" )
17+ NO_UPLOAD = os .getenv ("NO_UPLOAD" )
18+
19+ def find_cmd (env , exe ):
20+ cmd = os .getenv (env )
21+ if cmd :
22+ return Path (cmd )
23+ for p in os .getenv ("PATH" , "" ).split (";" ):
24+ if p :
25+ cmd = Path (p ) / exe
26+ if cmd .is_file ():
27+ return cmd
28+ if UPLOAD_HOST :
29+ raise RuntimeError (f"Could not find { exe } to perform upload. Try setting %{ env } % or %PATH%" )
30+ print (f"Did not find { exe } , but not uploading anyway." )
31+
32+ PLINK = find_cmd ("PLINK" , "plink.exe" )
33+ PSCP = find_cmd ("PSCP" , "pscp.exe" )
34+
35+
36+ def _std_args (cmd ):
37+ if not cmd :
38+ raise RuntimeError ("Cannot upload because command is missing" )
39+ all_args = [cmd , "-batch" ]
40+ if UPLOAD_HOST_KEY :
41+ all_args .append ("-hostkey" )
42+ all_args .append (UPLOAD_HOST_KEY )
43+ if UPLOAD_KEYFILE :
44+ all_args .append ("-noagent" )
45+ all_args .append ("-i" )
46+ all_args .append (UPLOAD_KEYFILE )
47+ return all_args
48+
49+
50+ class RunError (Exception ):
51+ pass
52+
53+
54+ def _run (* args ):
55+ with subprocess .Popen (
56+ args ,
57+ stdout = subprocess .PIPE ,
58+ stderr = subprocess .PIPE ,
59+ encoding = "ascii" ,
60+ errors = "replace" ,
61+ ) as p :
62+ out , err = p .communicate (None )
63+ if out :
64+ print (out )
65+ if err :
66+ print (err )
67+ if p .returncode :
68+ raise RunError (p .returncode , out , err )
69+
70+
71+ def call_ssh (* args , allow_fail = True ):
72+ if not UPLOAD_HOST or NO_UPLOAD :
73+ print ("Skipping" , args , "because UPLOAD_HOST is missing" )
74+ return
75+ try :
76+ _run (* _std_args (PLINK ), f"{ UPLOAD_USER } @{ UPLOAD_HOST } " , * args )
77+ except RunError :
78+ if not allow_fail :
79+ raise
80+
81+
82+ def upload_ssh (source , dest ):
83+ if not UPLOAD_HOST or NO_UPLOAD :
84+ print ("Skipping upload of" , source , "because UPLOAD_HOST is missing" )
85+ return
86+ _run (* _std_args (PSCP ), source , f"{ UPLOAD_USER } @{ UPLOAD_HOST } :{ dest } " )
1387
1488
15- def call_ssh ( args ):
89+ def download_ssh ( source , dest ):
1690 if not UPLOAD_HOST :
17- print ("Skipping" , args , "because UPLOAD_HOST is missing" )
91+ print ("Skipping download of" , source , "because UPLOAD_HOST is missing" )
92+ return
93+ _run (* _std_args (PSCP ), f"{ UPLOAD_USER } @{ UPLOAD_HOST } :{ source } " , dest )
94+
95+
96+ def ls_ssh (dest ):
97+ if not UPLOAD_HOST :
98+ print ("Skipping ls of" , dest , "because UPLOAD_HOST is missing" )
1899 return
19- subprocess .check_output (args )
100+ try :
101+ _run (* _std_args (PSCP ), "-ls" , f"{ UPLOAD_USER } @{ UPLOAD_HOST } :{ dest } " )
102+ except RunError as ex :
103+ if not ex .args [2 ].rstrip ().endswith ("No such file or directory" ):
104+ raise
105+ print (dest , "was not found" )
106+
107+
108+ def url2path (url ):
109+ if not url .startswith (UPLOAD_URL_PREFIX ):
110+ raise ValueError (f"Unexpected URL: { url } " )
111+ return UPLOAD_PATH_PREFIX + url [len (UPLOAD_URL_PREFIX ):]
20112
21113
22114def get_hashes (src ):
@@ -30,7 +122,7 @@ def get_hashes(src):
30122
31123
32124def purge (url ):
33- if not UPLOAD_HOST :
125+ if not UPLOAD_HOST or NO_UPLOAD :
34126 print ("Skipping purge of" , url , "because UPLOAD_HOST is missing" )
35127 return
36128 with urlopen (Request (url , method = "PURGE" , headers = {"Fastly-Soft-Purge" : 1 })) as r :
@@ -41,52 +133,73 @@ def calculate_uploads():
41133 for p in Path ().absolute ().glob ("__install*.json" ):
42134 i = json .loads (p .read_bytes ())
43135 u = urlparse (i ["url" ])
136+ src = Path (u .path .rpartition ("/" )[- 1 ]).absolute ()
137+ dest = url2path (i ["url" ])
138+ sbom = src .with_suffix (".spdx.json" )
139+ sbom_dest = dest .rpartition ("/" )[0 ] + sbom .name
140+ if not sbom .is_file ():
141+ sbom = None
142+ sbom_dest = None
44143 yield (
45144 i ,
46- Path (u .path .rpartition ("/" )[- 1 ]).absolute (),
47- u .path ,
145+ src ,
146+ url2path (i ["url" ]),
147+ sbom ,
148+ sbom_dest ,
48149 )
49150
50- def upload_files (uploads ):
51- for i , src , dest in uploads :
52- print ("Uploading" , src , "to" , dest )
53- call_ssh ([...])
54-
55- def purge_files (uploads ):
56- for i , src , dest in uploads :
57- purge (i ["url" ])
58151
59152def hash_packages (uploads ):
60- for i , src , dest in uploads :
153+ for i , src , * _ in uploads :
61154 i ["hashes" ] = get_hashes (src )
62155
63156
64157UPLOADS = list (calculate_uploads ())
65158hash_packages (UPLOADS )
66159
67160
161+ INDEX_PATH = url2path (INDEX_URL )
68162try :
69- with open ("__index__.json" , "rb" ) as f :
70- index = json .load (f )
71- except FileNotFoundError :
163+ download_ssh (INDEX_PATH , "__index__.json" )
164+ except RunError as ex :
165+ err = ex .args [2 ]
166+ if not err .rstrip ().endswith ("no such file or directory" ):
167+ raise
72168 index = {"versions" : []}
169+ else :
170+ with open ("__index__.json" , "r" , encoding = "utf-8" ) as f :
171+ index = json .load (f )
73172
74173
75174# TODO: Sort?
76- index ["versions" ][:0 ] = [i [0 ] for i in UPLOADS ]
175+ index ["versions" ][:0 ] = [i for i , * _ in UPLOADS ]
176+
77177
78- with open ("__index__.json" , "wb " ) as f :
178+ with open ("__index__.json" , "w" , encoding = "utf-8 " ) as f :
79179 # Include an indent for sanity while testing.
80180 # We should probably remove it later for the size benefits.
81- json .dump (f , index , indent = 1 )
181+ json .dump (index , f , indent = 1 )
82182
83183print ("Merged" , len (UPLOADS ), "entries" )
84184
185+
85186# Upload last to ensure we've got a valid index first
86- upload_files (UPLOADS )
187+ for i , src , dest , sbom , sbom_dest in UPLOADS :
188+ print ("Uploading" , src , "to" , dest )
189+ destdir = dest .rpartition ("/" )[0 ]
190+ call_ssh ("mkdir" , destdir , "&&" , "chgrp" , "downloads" , destdir , "&&" , "chmod" , "a+rx" , destdir )
191+ upload_ssh (src , dest )
192+ call_ssh ("chgrp" , "downloads" , dest , "&&" , "chmod" , "g-x,o+r" , dest )
193+ if sbom and sbom_dest :
194+ upload_ssh (sbom , sbom_dest )
195+ call_ssh ("chgrp" , "downloads" , sbom_dest , "&&" , "chmod" , "g-x,o+r" , sbom_dest )
196+
197+
198+ print ("Uploading __index__.json to" , INDEX_URL )
199+ upload_ssh ("__index__.json" , INDEX_PATH )
87200
88- print ("Uploading __index__.json to python.org" )
89- run_ssh ([...])
90201
91- purge_files (UPLOADS )
202+ print ("Purging" , len (UPLOADS ), "uploaded files" )
203+ for i , src , dest in UPLOADS :
204+ purge (i ["url" ])
92205purge (INDEX_URL )
0 commit comments