1*2cb6d6f1SJim Jagielski################################################################ 2*2cb6d6f1SJim Jagielski# 3*2cb6d6f1SJim Jagielski# Licensed to the Apache Software Foundation (ASF) under one 4*2cb6d6f1SJim Jagielski# or more contributor license agreements. See the NOTICE file 5*2cb6d6f1SJim Jagielski# distributed with this work for additional information 6*2cb6d6f1SJim Jagielski# regarding copyright ownership. The ASF licenses this file 7*2cb6d6f1SJim Jagielski# to you under the Apache License, Version 2.0 (the 8*2cb6d6f1SJim Jagielski# "License"); you may not use this file except in compliance 9*2cb6d6f1SJim Jagielski# with the License. You may obtain a copy of the License at 10*2cb6d6f1SJim Jagielski# 11*2cb6d6f1SJim Jagielski# http://www.apache.org/licenses/LICENSE-2.0 12*2cb6d6f1SJim Jagielski# 13*2cb6d6f1SJim Jagielski# Unless required by applicable law or agreed to in writing, 14*2cb6d6f1SJim Jagielski# software distributed under the License is distributed on an 15*2cb6d6f1SJim Jagielski# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16*2cb6d6f1SJim Jagielski# KIND, either express or implied. See the License for the 17*2cb6d6f1SJim Jagielski# specific language governing permissions and limitations 18*2cb6d6f1SJim Jagielski# under the License. 19*2cb6d6f1SJim Jagielski# 20*2cb6d6f1SJim Jagielski################################################################ 21*2cb6d6f1SJim Jagielski 22*2cb6d6f1SJim Jagielski"""Downloads the items listed in data/fetch.json (OASIS schemas, jing, odfvalidator) into a per-user cache. 23*2cb6d6f1SJim Jagielski 24*2cb6d6f1SJim JagielskiEvery file is checked against its pinned sha256 before it is moved into place, so a cached file is always a verified one. 25*2cb6d6f1SJim JagielskiNothing third-party is committed. Command line: python -m aootest.fetch [--offline] [--list] [name ...] 26*2cb6d6f1SJim Jagielski""" 27*2cb6d6f1SJim Jagielskiimport hashlib 28*2cb6d6f1SJim Jagielskiimport json 29*2cb6d6f1SJim Jagielskiimport os 30*2cb6d6f1SJim Jagielskiimport sys 31*2cb6d6f1SJim Jagielskiimport tempfile 32*2cb6d6f1SJim Jagielskiimport urllib.request 33*2cb6d6f1SJim Jagielskifrom pathlib import Path 34*2cb6d6f1SJim Jagielski 35*2cb6d6f1SJim Jagielskifrom aootest.paths import DATA 36*2cb6d6f1SJim Jagielski 37*2cb6d6f1SJim Jagielski 38*2cb6d6f1SJim Jagielskidef cache_dir(): 39*2cb6d6f1SJim Jagielski """AOO_TEST_CACHE, else the usual per-user cache directory of the platform.""" 40*2cb6d6f1SJim Jagielski env = os.environ.get("AOO_TEST_CACHE") 41*2cb6d6f1SJim Jagielski if env: 42*2cb6d6f1SJim Jagielski return Path(env) 43*2cb6d6f1SJim Jagielski if sys.platform == "win32": 44*2cb6d6f1SJim Jagielski return Path(os.environ.get("LOCALAPPDATA") or Path.home() / "AppData" / "Local") / "aootest" / "Cache" 45*2cb6d6f1SJim Jagielski if sys.platform == "darwin": 46*2cb6d6f1SJim Jagielski return Path.home() / "Library" / "Caches" / "aootest" 47*2cb6d6f1SJim Jagielski return Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "aootest" 48*2cb6d6f1SJim Jagielski 49*2cb6d6f1SJim Jagielski 50*2cb6d6f1SJim Jagielskidef load_items(path=None): 51*2cb6d6f1SJim Jagielski """The entries of fetch.json: dicts with name, group, url, sha256, path (relative to the cache), license.""" 52*2cb6d6f1SJim Jagielski return json.loads(Path(path or DATA / "fetch.json").read_text(encoding="utf-8"))["items"] 53*2cb6d6f1SJim Jagielski 54*2cb6d6f1SJim Jagielski 55*2cb6d6f1SJim Jagielskidef sha256_of(path): 56*2cb6d6f1SJim Jagielski h = hashlib.sha256() 57*2cb6d6f1SJim Jagielski with open(path, "rb") as f: 58*2cb6d6f1SJim Jagielski for block in iter(lambda: f.read(1 << 20), b""): 59*2cb6d6f1SJim Jagielski h.update(block) 60*2cb6d6f1SJim Jagielski return h.hexdigest() 61*2cb6d6f1SJim Jagielski 62*2cb6d6f1SJim Jagielski 63*2cb6d6f1SJim Jagielskidef status(item, cache=None): 64*2cb6d6f1SJim Jagielski """'ok' (present and matching), 'bad' (present, wrong checksum) or 'missing'.""" 65*2cb6d6f1SJim Jagielski p = Path(cache or cache_dir()) / item["path"] 66*2cb6d6f1SJim Jagielski if not p.is_file(): 67*2cb6d6f1SJim Jagielski return "missing" 68*2cb6d6f1SJim Jagielski return "ok" if sha256_of(p) == item["sha256"] else "bad" 69*2cb6d6f1SJim Jagielski 70*2cb6d6f1SJim Jagielski 71*2cb6d6f1SJim Jagielskidef cached_path(name, cache=None, items=None): 72*2cb6d6f1SJim Jagielski """Path of a verified cached item, or None.""" 73*2cb6d6f1SJim Jagielski for it in items or load_items(): 74*2cb6d6f1SJim Jagielski if it["name"] == name: 75*2cb6d6f1SJim Jagielski return Path(cache or cache_dir()) / it["path"] if status(it, cache) == "ok" else None 76*2cb6d6f1SJim Jagielski raise KeyError(name) 77*2cb6d6f1SJim Jagielski 78*2cb6d6f1SJim Jagielski 79*2cb6d6f1SJim Jagielskidef group_dir(group, cache=None, items=None): 80*2cb6d6f1SJim Jagielski """The directory holding a group (all files in one directory) when every item of it is verified, else None.""" 81*2cb6d6f1SJim Jagielski its = [it for it in (items or load_items()) if it["group"] == group] 82*2cb6d6f1SJim Jagielski if not its or any(status(it, cache) != "ok" for it in its): 83*2cb6d6f1SJim Jagielski return None 84*2cb6d6f1SJim Jagielski dirs = {(Path(cache or cache_dir()) / it["path"]).parent for it in its} 85*2cb6d6f1SJim Jagielski return dirs.pop() if len(dirs) == 1 else None 86*2cb6d6f1SJim Jagielski 87*2cb6d6f1SJim Jagielski 88*2cb6d6f1SJim Jagielskidef _download(item, cache, opener): 89*2cb6d6f1SJim Jagielski url = item["url"] 90*2cb6d6f1SJim Jagielski if not url.startswith("https://"): 91*2cb6d6f1SJim Jagielski raise ValueError("only https URLs are fetched: " + url) 92*2cb6d6f1SJim Jagielski dest = Path(cache) / item["path"] 93*2cb6d6f1SJim Jagielski dest.parent.mkdir(parents=True, exist_ok=True) 94*2cb6d6f1SJim Jagielski fd, tmp = tempfile.mkstemp(dir=dest.parent, prefix=dest.name + ".part") 95*2cb6d6f1SJim Jagielski try: 96*2cb6d6f1SJim Jagielski h = hashlib.sha256() 97*2cb6d6f1SJim Jagielski with os.fdopen(fd, "wb") as out, opener(urllib.request.Request(url, headers={"User-Agent": "aootest-fetch"})) as src: 98*2cb6d6f1SJim Jagielski for block in iter(lambda: src.read(1 << 20), b""): 99*2cb6d6f1SJim Jagielski h.update(block) 100*2cb6d6f1SJim Jagielski out.write(block) 101*2cb6d6f1SJim Jagielski if h.hexdigest() != item["sha256"]: 102*2cb6d6f1SJim Jagielski raise ValueError("checksum mismatch for %s: got %s" % (url, h.hexdigest())) 103*2cb6d6f1SJim Jagielski os.replace(tmp, dest) 104*2cb6d6f1SJim Jagielski except BaseException: 105*2cb6d6f1SJim Jagielski if os.path.exists(tmp): 106*2cb6d6f1SJim Jagielski os.unlink(tmp) 107*2cb6d6f1SJim Jagielski raise 108*2cb6d6f1SJim Jagielski 109*2cb6d6f1SJim Jagielski 110*2cb6d6f1SJim Jagielskidef fetch(names=None, offline=False, cache=None, opener=None, items=None): 111*2cb6d6f1SJim Jagielski """Makes the named items (default: all) present. Returns {name: 'present' | 'downloaded' | 'missing' | 'failed: reason'}. 112*2cb6d6f1SJim Jagielski With offline=True nothing is downloaded and absent items are reported 'missing'. A file with the wrong checksum is 113*2cb6d6f1SJim Jagielski replaced only by a download that verifies.""" 114*2cb6d6f1SJim Jagielski cache = Path(cache or cache_dir()) 115*2cb6d6f1SJim Jagielski opener = opener or (lambda req: urllib.request.urlopen(req, timeout=60)) 116*2cb6d6f1SJim Jagielski result = {} 117*2cb6d6f1SJim Jagielski for it in items or load_items(): 118*2cb6d6f1SJim Jagielski if names and it["name"] not in names: 119*2cb6d6f1SJim Jagielski continue 120*2cb6d6f1SJim Jagielski if status(it, cache) == "ok": 121*2cb6d6f1SJim Jagielski result[it["name"]] = "present" 122*2cb6d6f1SJim Jagielski elif offline: 123*2cb6d6f1SJim Jagielski result[it["name"]] = "missing" 124*2cb6d6f1SJim Jagielski else: 125*2cb6d6f1SJim Jagielski try: 126*2cb6d6f1SJim Jagielski _download(it, cache, opener) 127*2cb6d6f1SJim Jagielski result[it["name"]] = "downloaded" 128*2cb6d6f1SJim Jagielski except Exception as e: 129*2cb6d6f1SJim Jagielski result[it["name"]] = "failed: %s" % e 130*2cb6d6f1SJim Jagielski return result 131*2cb6d6f1SJim Jagielski 132*2cb6d6f1SJim Jagielski 133*2cb6d6f1SJim Jagielskidef main(argv=None): 134*2cb6d6f1SJim Jagielski argv = sys.argv[1:] if argv is None else list(argv) 135*2cb6d6f1SJim Jagielski offline, listing = "--offline" in argv, "--list" in argv 136*2cb6d6f1SJim Jagielski names = [a for a in argv if not a.startswith("--")] 137*2cb6d6f1SJim Jagielski unknown = set(names) - {it["name"] for it in load_items()} 138*2cb6d6f1SJim Jagielski if unknown: 139*2cb6d6f1SJim Jagielski print("unknown item: " + ", ".join(sorted(unknown)), file=sys.stderr) 140*2cb6d6f1SJim Jagielski return 2 141*2cb6d6f1SJim Jagielski if listing: 142*2cb6d6f1SJim Jagielski for it in load_items(): 143*2cb6d6f1SJim Jagielski print("%-9s %-8s %s" % (status(it), it["group"], it["name"])) 144*2cb6d6f1SJim Jagielski return 0 145*2cb6d6f1SJim Jagielski res = fetch(names or None, offline=offline) 146*2cb6d6f1SJim Jagielski for n, s in res.items(): 147*2cb6d6f1SJim Jagielski print("%-12s %s" % (s.split(":")[0], n) + (s[s.index(":"):] if s.startswith("failed") else "")) 148*2cb6d6f1SJim Jagielski print("cache: %s" % cache_dir()) 149*2cb6d6f1SJim Jagielski return 0 if all(s in ("present", "downloaded") for s in res.values()) else 1 150*2cb6d6f1SJim Jagielski 151*2cb6d6f1SJim Jagielski 152*2cb6d6f1SJim Jagielskiif __name__ == "__main__": 153*2cb6d6f1SJim Jagielski sys.exit(main()) 154