| 
 | 1 | +#!/usr/bin/env python3  | 
 | 2 | + | 
 | 3 | +'''  | 
 | 4 | +export-contest -- Convenience script to export a contest (including metadata,  | 
 | 5 | +teams and problems) from the command line. Defaults to using the CLI interface;  | 
 | 6 | +Specify a DOMjudge API URL as to use that.  | 
 | 7 | +
  | 
 | 8 | +Reads credentials from ~/.netrc when using the API.  | 
 | 9 | +
  | 
 | 10 | +Part of the DOMjudge Programming Contest Jury System and licensed  | 
 | 11 | +under the GNU GPL. See README and COPYING for details.  | 
 | 12 | +'''  | 
 | 13 | + | 
 | 14 | +import json  | 
 | 15 | +import sys  | 
 | 16 | +from concurrent.futures import ThreadPoolExecutor, as_completed  | 
 | 17 | +from pathlib import Path  | 
 | 18 | + | 
 | 19 | +sys.path.append('@domserver_libdir@')  | 
 | 20 | +import dj_utils  | 
 | 21 | + | 
 | 22 | +mime_to_extension = {  | 
 | 23 | +    'application/pdf': 'pdf',  | 
 | 24 | +    'application/zip': 'zip',  | 
 | 25 | +    'image/jpeg': 'jpg',  | 
 | 26 | +    'image/png': 'png',  | 
 | 27 | +    'image/svg+xml': 'svg',  | 
 | 28 | +    'text/plain': 'txt',  | 
 | 29 | +    'video/mp4': 'mp4',  | 
 | 30 | +    'video/mpeg': 'mpg',  | 
 | 31 | +    'video/webm': 'webm',  | 
 | 32 | +}  | 
 | 33 | + | 
 | 34 | + | 
 | 35 | +def usage():  | 
 | 36 | +    print(f'Usage: {sys.argv[0]} [<domjudge-api-url>]')  | 
 | 37 | +    exit(1)  | 
 | 38 | + | 
 | 39 | + | 
 | 40 | +def download_file(file: dict, dir: str, default_name: str):  | 
 | 41 | +    print(f"Downloading '{file['href']}'")  | 
 | 42 | +    Path(dir).mkdir(parents=True, exist_ok=True)  | 
 | 43 | +    filename = file['filename'] if 'filename' in file else default_name  | 
 | 44 | +    dj_utils.do_api_request(file['href'], decode=False, output_file=f'{dir}/{filename}')  | 
 | 45 | + | 
 | 46 | + | 
 | 47 | +def is_file(data) -> bool:  | 
 | 48 | +    '''  | 
 | 49 | +    Check whether API data represents a FILE object. This is heuristic because  | 
 | 50 | +    no property is strictly required, but we need at least `href` to download  | 
 | 51 | +    the file, so if also we find one other property, we announce a winner.  | 
 | 52 | +    '''  | 
 | 53 | +    if not isinstance(data, dict):  | 
 | 54 | +        return false  | 
 | 55 | +    return 'href' in data and ('mime' in data or 'filename' in data or 'hash' in data)  | 
 | 56 | + | 
 | 57 | + | 
 | 58 | +files_to_download = []  | 
 | 59 | + | 
 | 60 | +def recurse_find_files(data, store_path: str, default_name: str):  | 
 | 61 | +    if isinstance(data, list):  | 
 | 62 | +        # Special case single element list for simpler default_name  | 
 | 63 | +        if len(data) == 1:  | 
 | 64 | +            recurse_find_files(data[0], store_path, default_name)  | 
 | 65 | +        else:  | 
 | 66 | +            for i, item in enumerate(data):  | 
 | 67 | +                recurse_find_files(item, store_path, f"{default_name}.{i}")  | 
 | 68 | +    elif isinstance(data, dict):  | 
 | 69 | +        if is_file(data):  | 
 | 70 | +            if 'mime' in data and data['mime'] in mime_to_extension:  | 
 | 71 | +                default_name += '.' + mime_to_extension[data['mime']]  | 
 | 72 | +            files_to_download.append((data, store_path, default_name))  | 
 | 73 | +        else:  | 
 | 74 | +            for key, item in data.items():  | 
 | 75 | +                recurse_find_files(item, store_path, f"{default_name}.{key}")  | 
 | 76 | + | 
 | 77 | + | 
 | 78 | +def download_endpoint(name: str, path: str):  | 
 | 79 | +    ext = '.ndjson' if name == 'event-feed' else '.json'  | 
 | 80 | +    filename = name + ext  | 
 | 81 | + | 
 | 82 | +    print(f"Fetching '{path}' to '{filename}'")  | 
 | 83 | +    data = dj_utils.do_api_request(path, decode=False)  | 
 | 84 | +    with open(filename, 'wb') as f:  | 
 | 85 | +        f.write(data)  | 
 | 86 | + | 
 | 87 | +    if ext == '.json':  | 
 | 88 | +        data = json.loads(data)  | 
 | 89 | +        store_path = name  | 
 | 90 | +        if isinstance(data, list):  | 
 | 91 | +            for elem in data:  | 
 | 92 | +                recurse_find_files(elem, f"{store_path}/{elem['id']}", '')  | 
 | 93 | +        else:  | 
 | 94 | +            recurse_find_files(data, store_path, '')  | 
 | 95 | + | 
 | 96 | + | 
 | 97 | +if len(sys.argv) == 2:  | 
 | 98 | +    dj_utils.domjudge_api_url = sys.argv[1]  | 
 | 99 | +elif len(sys.argv) != 1:  | 
 | 100 | +    usage()  | 
 | 101 | + | 
 | 102 | + | 
 | 103 | +user_data = dj_utils.do_api_request('user')  | 
 | 104 | +if 'admin' not in user_data['roles']:  | 
 | 105 | +    print('Your user does not have the \'admin\' role, can not export.')  | 
 | 106 | +    exit(1)  | 
 | 107 | + | 
 | 108 | + | 
 | 109 | +contest_id = 'wf48_finals'  | 
 | 110 | +contest_path = f'contests/{contest_id}'  | 
 | 111 | + | 
 | 112 | +# Custom endpoints:  | 
 | 113 | +download_endpoint('api', '')  | 
 | 114 | +download_endpoint('contest', contest_path)  | 
 | 115 | +download_endpoint('event-feed', f'{contest_path}/event-feed?stream=false')  | 
 | 116 | + | 
 | 117 | +for endpoint in [  | 
 | 118 | +        'access',  | 
 | 119 | +        'accounts',  | 
 | 120 | +        'awards',  | 
 | 121 | +#        'balloons', This is a DOMjudge specific endpoint  | 
 | 122 | +        'clarifications',  | 
 | 123 | +#        'commentary', Not implemented in DOMjudge  | 
 | 124 | +        'groups',  | 
 | 125 | +        'judgement-types',  | 
 | 126 | +        'judgements',  | 
 | 127 | +        'languages',  | 
 | 128 | +        'organizations',  | 
 | 129 | +#        'persons', Not implemented in DOMjudge  | 
 | 130 | +        'problems',  | 
 | 131 | +        'runs',  | 
 | 132 | +        'scoreboard',  | 
 | 133 | +        'state',  | 
 | 134 | +        'submissions',  | 
 | 135 | +        'teams',  | 
 | 136 | +        ]:  | 
 | 137 | +    download_endpoint(endpoint, f"{contest_path}/{endpoint}")  | 
 | 138 | + | 
 | 139 | +with ThreadPoolExecutor(20) as executor:  | 
 | 140 | +    futures = [executor.submit(download_file, *item) for item in files_to_download]  | 
 | 141 | +    for future in as_completed(futures):  | 
 | 142 | +        future.result() # So it can throw any exception  | 
0 commit comments