xref: /trunk/test/aoo-test/src/aootest/fetch.py (revision 2cb6d6f119bbe2f78742f1c5c8538cb0d4e72ede)
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