annotate scripts/hvresources/uploader.py @ 540:67a746eea29e prerelease

Updated uploader.py
author Giulio Moro <giuliomoro@yahoo.it>
date Fri, 24 Jun 2016 02:12:35 +0100
parents a084456960c9
children ff0e9e827dcd
rev   line source
giuliomoro@229 1 # Copyright 2015,2016 Enzien Audio, Ltd. All Rights Reserved.
chris@160 2
chris@160 3 import argparse
giuliomoro@540 4 import datetime
chris@160 5 import getpass
chris@160 6 import json
chris@160 7 import os
chris@160 8 import requests
chris@160 9 import shutil
chris@160 10 import stat
chris@160 11 import tempfile
chris@160 12 import time
chris@160 13 import urlparse
chris@160 14 import zipfile
chris@160 15
chris@160 16 class Colours:
chris@160 17 purple = "\033[95m"
chris@160 18 cyan = "\033[96m"
chris@160 19 dark_cyan = "\033[36m"
chris@160 20 blue = "\033[94m"
chris@160 21 green = "\033[92m"
chris@160 22 yellow = "\033[93m"
chris@160 23 red = "\033[91m"
chris@160 24 bold = "\033[1m"
chris@160 25 underline = "\033[4m"
chris@160 26 end = "\033[0m"
chris@160 27
giuliomoro@403 28 # the maxmimum file upload size of 1MB
giuliomoro@403 29 __HV_MAX_UPLOAD_SIZE = 1024*1024
giuliomoro@403 30
chris@160 31 def __zip_dir(in_dir, zip_path, file_filter=None):
giuliomoro@403 32 """Recursively zip an entire directory with an optional file filter
giuliomoro@403 33 """
chris@160 34 zf = zipfile.ZipFile(zip_path, mode="w", compression=zipfile.ZIP_DEFLATED)
chris@160 35 for subdir, dirs, files in os.walk(in_dir):
giuliomoro@403 36 for f in files:
giuliomoro@403 37 if (file_filter is None) or (f.lower().split(".")[-1] in file_filter):
chris@160 38 zf.write(
giuliomoro@403 39 filename=os.path.join(subdir,f),
giuliomoro@403 40 arcname=os.path.relpath(os.path.join(subdir,f), start=in_dir))
chris@160 41 return zip_path
chris@160 42
chris@160 43 def __unzip(zip_path, target_dir):
chris@160 44 """Unzip a file to a given directory. All destination files are overwritten.
chris@160 45 """
chris@160 46 zipfile.ZipFile(zip_path).extractall(target_dir)
chris@160 47
chris@160 48 def main():
chris@160 49 parser = argparse.ArgumentParser(
chris@160 50 description="Compiles a Pure Data file.")
chris@160 51 parser.add_argument(
chris@160 52 "input_dir",
giuliomoro@229 53 help="A directory containing _main.pd. All .pd files in the directory structure will be uploaded.")
chris@160 54 parser.add_argument(
chris@160 55 "-n", "--name",
chris@160 56 default="heavy",
giuliomoro@229 57 help="Patch name. If it doesn't exist on the Heavy site, the uploader will fail.")
chris@160 58 parser.add_argument(
chris@160 59 "-g", "--gen",
chris@160 60 nargs="+",
chris@160 61 default=["c"],
giuliomoro@229 62 help="List of generator outputs. Currently supported generators are "
giuliomoro@229 63 "'c', 'js', 'pdext', 'pdext-osx', 'unity', 'unity-osx', "
giuliomoro@229 64 "'unity-win-x86', 'unity-win-x86_64', 'wwise', 'wwise-win-x86_64', "
giuliomoro@229 65 "'vst2' ,'vst2-osx', and 'vst2-win-x86_64'.")
chris@160 66 parser.add_argument(
chris@160 67 "-b",
giuliomoro@229 68 help="All files will be placed in the output directory, placed in their own subdirectory corresponding to the generator name.",
giuliomoro@229 69 action="count")
giuliomoro@229 70 parser.add_argument(
giuliomoro@229 71 "-y",
giuliomoro@229 72 help="Extract only the generated C files. Static files are deleted. "
giuliomoro@229 73 "Only effective for the 'c' generator.",
chris@160 74 action="count")
chris@160 75 parser.add_argument(
chris@160 76 "-o", "--out",
chris@160 77 nargs="+",
chris@160 78 default=["./"], # by default
chris@160 79 help="List of destination directories for retrieved files. Order should be the same as for --gen.")
chris@160 80 parser.add_argument(
giuliomoro@403 81 "-r", "--release",
giuliomoro@403 82 help="Optionally request a specific release of Heavy to use while compiling.")
giuliomoro@403 83 parser.add_argument(
chris@160 84 "-d", "--domain",
chris@160 85 default="https://enzienaudio.com",
chris@160 86 help="Domain. Default is https://enzienaudio.com.")
chris@160 87 parser.add_argument(
chris@160 88 "-x",
chris@160 89 help="Don't save the returned token.",
chris@160 90 action="count")
chris@160 91 parser.add_argument(
chris@160 92 "-z",
chris@160 93 help="Force the use of a password, regardless of saved token.",
chris@160 94 action="count")
chris@160 95 parser.add_argument(
chris@160 96 "--noverify",
giuliomoro@229 97 help="Don't verify the SSL connection. This is generally a very bad idea.",
chris@160 98 action="count")
chris@160 99 parser.add_argument(
chris@160 100 "-v", "--verbose",
chris@160 101 help="Show debugging information.",
chris@160 102 action="count")
giuliomoro@229 103 parser.add_argument(
giuliomoro@229 104 "-t", "--token",
giuliomoro@229 105 help="Use the specified token.",
giuliomoro@229 106 )
chris@160 107 args = parser.parse_args()
chris@160 108
chris@160 109 domain = args.domain or "https://enzienaudio.com"
chris@160 110
chris@160 111 post_data = {}
chris@160 112
chris@160 113 # token should be stored in ~/.heavy/token
chris@160 114 token_path = os.path.expanduser(os.path.join("~/", ".heavy", "token"))
giuliomoro@229 115
giuliomoro@229 116 if args.token is not None:
giuliomoro@229 117 # check if token has been passed as a command line arg...
giuliomoro@229 118 post_data["credentials"] = {"token": args.token}
giuliomoro@229 119 elif os.path.exists(token_path) and not args.z:
giuliomoro@229 120 # ...or if it is stored in the user's home directory
chris@160 121 with open(token_path, "r") as f:
giuliomoro@229 122 post_data["credentials"] = {"token": f.read()}
chris@160 123 else:
chris@160 124 # otherwise, get the username and password
chris@160 125 post_data["credentials"] = {
chris@160 126 "username": raw_input("Enter username: "),
chris@160 127 "password": getpass.getpass("Enter password: ")
chris@160 128 }
chris@160 129
chris@160 130 tick = time.time()
chris@160 131
giuliomoro@403 132 # parse the optional release argument
giuliomoro@403 133 if args.release:
giuliomoro@540 134 try:
giuliomoro@540 135 # check the validity of the current release
giuliomoro@540 136 releases_json = requests.get(urlparse.urljoin(domain, "/a/releases")).json()
giuliomoro@540 137 if args.release in releases_json:
giuliomoro@540 138 today = datetime.datetime.now()
giuliomoro@540 139 valid_until = datetime.datetime.strptime(releases_json[args.release]["validUntil"], "%Y-%m-%d")
giuliomoro@540 140 if today > valid_until:
giuliomoro@540 141 print "{0}Warning:{1} The release \"{2}\" expired on {3}. It may be removed at any time!".format(
giuliomoro@540 142 Colours.yellow, Colours.end,
giuliomoro@540 143 args.release,
giuliomoro@540 144 releases_json[args.release]["validUntil"])
giuliomoro@540 145 elif (valid_until - today) <= datetime.timedelta(weeks=4):
giuliomoro@540 146 print "{0}Warning:{1} The release \"{2}\" will expire soon on {3}.".format(
giuliomoro@540 147 Colours.yellow, Colours.end,
giuliomoro@540 148 args.release,
giuliomoro@540 149 releases_json[args.release]["validUntil"])
giuliomoro@540 150 else:
giuliomoro@540 151 print "{0}Error:{1} The release \"{2}\" is not available. Available releases are:".format(
giuliomoro@540 152 Colours.red, Colours.end,
giuliomoro@540 153 args.release)
giuliomoro@540 154 for k,v in releases_json.items():
giuliomoro@540 155 print "* {0} ({1})".format(
giuliomoro@540 156 k,
giuliomoro@540 157 v["releaseDate"])
giuliomoro@540 158 return
giuliomoro@540 159 except:
giuliomoro@540 160 pass # if the /a/releases request fails for whatever reason, just move on
giuliomoro@540 161
giuliomoro@403 162 post_data["release"] = args.release
giuliomoro@403 163
chris@160 164 # make a temporary directory
chris@160 165 temp_dir = tempfile.mkdtemp(prefix="lroyal-")
chris@160 166
chris@160 167 # zip up the pd directory into the temporary directory
chris@160 168 try:
chris@160 169 if not os.path.exists(os.path.join(args.input_dir, "_main.pd")):
chris@160 170 raise Exception("Root Pd directory does not contain a file named _main.pd.")
chris@160 171 zip_path = __zip_dir(
chris@160 172 args.input_dir,
chris@160 173 os.path.join(temp_dir, "archive.zip"),
chris@160 174 file_filter={"pd"})
giuliomoro@403 175 if os.stat(zip_path).st_size > __HV_MAX_UPLOAD_SIZE:
giuliomoro@403 176 raise Exception("The target directory, zipped, is {0} bytes. The maximum upload size of 1MB.".format(
giuliomoro@403 177 os.stat(zip_path).st_size))
chris@160 178 except Exception as e:
giuliomoro@403 179 print "{0}Error:{1} {2}".format(Colours.red, Colours.end, e)
chris@160 180 shutil.rmtree(temp_dir) # clean up the temporary directory
giuliomoro@522 181 return
chris@160 182
chris@160 183 post_data["name"] = args.name
chris@160 184
chris@160 185 # the outputs to generate (always include c)
giuliomoro@229 186 __SUPPORTED_GENERATOR_SET = {
giuliomoro@229 187 "c", "js",
giuliomoro@229 188 "pdext", "pdext-osx",
giuliomoro@229 189 "unity", "unity-osx", "unity-win-x86", "unity-win-x86_64",
giuliomoro@229 190 "wwise", "wwise-win-x86_64",
giuliomoro@229 191 "vst2", "vst2-osx", "vst2-win-x86_64",
giuliomoro@229 192 }
giuliomoro@403 193 post_data["gen"] = list(({"c"} | {s.lower() for s in set(args.gen)}) & __SUPPORTED_GENERATOR_SET)
chris@160 194
chris@160 195 # upload the job, get the response back
chris@160 196 # NOTE(mhroth): multipart-encoded file can only be sent as a flat dictionary,
chris@160 197 # but we want to send a json encoded deep dictionary. So we do a bit of a hack.
chris@160 198 r = requests.post(
chris@160 199 urlparse.urljoin(domain, "/a/heavy"),
chris@160 200 data={"json":json.dumps(post_data)},
chris@160 201 files={"file": (os.path.basename(zip_path), open(zip_path, "rb"), "application/zip")},
chris@160 202 verify=False if args.noverify else True)
chris@160 203
chris@160 204 if r.status_code != requests.codes.ok:
chris@160 205 shutil.rmtree(temp_dir) # clean up the temporary directory
giuliomoro@229 206 print "Getting a weird error? Get the latest uploader at https://enzienaudio.com/static/uploader.py"
chris@160 207 r.raise_for_status() # raise an exception
chris@160 208
chris@160 209 # decode the JSON API response
chris@160 210 r_json = r.json()
chris@160 211
chris@160 212 """
chris@160 213 {
chris@160 214 "data": {
chris@160 215 "compileTime": 0.05078411102294922,
chris@160 216 "id": "mhroth/asdf/Edp2G",
chris@160 217 "slug": "Edp2G",
chris@160 218 "index": 3,
chris@160 219 "links": {
chris@160 220 "files": {
chris@160 221 "linkage": [
chris@160 222 {
chris@160 223 "id": "mhroth/asdf/Edp2G/c",
chris@160 224 "type": "file"
chris@160 225 }
chris@160 226 ],
chris@160 227 "self": "https://enzienaudio.com/h/mhroth/asdf/Edp2G/files"
chris@160 228 },
chris@160 229 "project": {
chris@160 230 "linkage": {
chris@160 231 "id": "mhroth/asdf",
chris@160 232 "type": "project"
chris@160 233 },
chris@160 234 "self": "https://enzienaudio.com/h/mhroth/asdf"
chris@160 235 },
chris@160 236 "self": "https://enzienaudio.com/h/mhroth/asdf/Edp2G",
chris@160 237 "user": {
chris@160 238 "linkage": {
chris@160 239 "id": "mhroth",
chris@160 240 "type": "user"
chris@160 241 },
chris@160 242 "self": "https://enzienaudio.com/h/mhroth"
chris@160 243 }
chris@160 244 },
chris@160 245 "type": "job"
chris@160 246 },
chris@160 247 "included": [
chris@160 248 {
chris@160 249 "filename": "file.c.zip",
chris@160 250 "generator": "c",
chris@160 251 "id": "mhroth/asdf/Edp2G/c",
chris@160 252 "links": {
chris@160 253 "self": "https://enzienaudio.com/h/mhroth/asdf/Edp2G/c/file.c.zip"
chris@160 254 },
chris@160 255 "mime": "application/zip",
chris@160 256 "type": "file"
chris@160 257 }
chris@160 258 ],
giuliomoro@229 259 "warnings": [
giuliomoro@229 260 {"details": "blah blah blah"}
giuliomoro@229 261 ],
chris@160 262 "meta": {
chris@160 263 "token": "11AS0qPRmjTUHEMSovPEvzjodnzB1xaz"
chris@160 264 }
chris@160 265 }
chris@160 266 """
chris@160 267 reply_json = r.json()
chris@160 268 if args.verbose:
chris@160 269 print json.dumps(
chris@160 270 reply_json,
chris@160 271 sort_keys=True,
chris@160 272 indent=2,
chris@160 273 separators=(",", ": "))
chris@160 274
chris@160 275 # update the api token, if present
chris@160 276 if "token" in reply_json.get("meta",{}) and not args.x:
giuliomoro@229 277 if args.token is not None:
giuliomoro@229 278 if reply_json["meta"]["token"] != args.token:
giuliomoro@229 279 print "WARNING: Token returned by API is not the same as the "
giuliomoro@229 280 "token supplied at the command line. (old = %s, new = %s)".format(
giuliomoro@229 281 args.token,
giuliomoro@229 282 reply_json["meta"]["token"])
giuliomoro@229 283 else:
giuliomoro@229 284 if not os.path.exists(os.path.dirname(token_path)):
giuliomoro@229 285 # ensure that the .heavy directory exists
giuliomoro@229 286 os.makedirs(os.path.dirname(token_path))
giuliomoro@229 287 with open(token_path, "w") as f:
giuliomoro@229 288 f.write(reply_json["meta"]["token"])
giuliomoro@229 289 # force rw------- permissions on the file
giuliomoro@229 290 os.chmod(token_path, stat.S_IRUSR | stat.S_IWUSR)
chris@160 291
chris@160 292 # print any warnings
giuliomoro@229 293 for i,x in enumerate(r_json.get("warnings",[])):
giuliomoro@229 294 print "{3}) {0}Warning:{1} {2}".format(
giuliomoro@229 295 Colours.yellow, Colours.end, x["detail"], i+1)
chris@160 296
chris@160 297 # check for errors
chris@160 298 if len(r_json.get("errors",[])) > 0:
chris@160 299 shutil.rmtree(temp_dir) # clean up the temporary directory
giuliomoro@229 300 for i,x in enumerate(r_json["errors"]):
giuliomoro@229 301 print "{3}) {0}Error:{1} {2}".format(
giuliomoro@229 302 Colours.red, Colours.end, x["detail"], i+1)
giuliomoro@522 303 return
chris@160 304
chris@160 305 # retrieve all requested files
chris@160 306 for i,g in enumerate(args.gen):
chris@160 307 file_url = __get_file_url_for_generator(reply_json, g)
chris@160 308 if file_url is not None and (len(args.out) > i or args.b):
chris@160 309 r = requests.get(
chris@160 310 file_url,
chris@160 311 cookies={"token": reply_json["meta"]["token"]},
chris@160 312 verify=False if args.noverify else True)
chris@160 313 r.raise_for_status()
chris@160 314
chris@160 315 # write the reply to a temporary file
chris@160 316 c_zip_path = os.path.join(temp_dir, "archive.{0}.zip".format(g))
chris@160 317 with open(c_zip_path, "wb") as f:
chris@160 318 f.write(r.content)
chris@160 319
chris@160 320 # unzip the files to where they belong
chris@160 321 if args.b:
chris@160 322 target_dir = os.path.join(os.path.abspath(os.path.expanduser(args.out[0])), g)
chris@160 323 else:
chris@160 324 target_dir = os.path.abspath(os.path.expanduser(args.out[i]))
chris@160 325 if not os.path.exists(target_dir):
chris@160 326 os.makedirs(target_dir) # ensure that the output directory exists
chris@160 327 __unzip(c_zip_path, target_dir)
chris@160 328
giuliomoro@229 329 if g == "c" and args.y:
giuliomoro@229 330 keep_files = ("_{0}.h".format(args.name), "_{0}.c".format(args.name))
giuliomoro@229 331 for f in os.listdir(target_dir):
giuliomoro@229 332 if not f.endswith(keep_files):
giuliomoro@229 333 os.remove(os.path.join(target_dir, f));
giuliomoro@229 334
chris@160 335 print "{0} files placed in {1}".format(g, target_dir)
chris@160 336 else:
chris@160 337 print "{0}Warning:{1} {2} files could not be retrieved.".format(
chris@160 338 Colours.yellow, Colours.end,
chris@160 339 g)
chris@160 340
chris@160 341 # delete the temporary directory
chris@160 342 shutil.rmtree(temp_dir)
chris@160 343
giuliomoro@229 344 print "Job URL:", reply_json["data"]["links"]["self"]
chris@160 345 print "Total request time: {0}ms".format(int(1000.0*(time.time()-tick)))
giuliomoro@522 346 print "Heavy release:", reply_json.get("meta",{}).get("release", "default")
chris@160 347
chris@160 348 def __get_file_url_for_generator(json_api, g):
chris@160 349 """Returns the file link for a specific generator.
chris@160 350 Returns None if no link could be found.
chris@160 351 """
chris@160 352 for i in json_api["included"]:
chris@160 353 if g == i["generator"]:
chris@160 354 return i["links"]["self"]
chris@160 355 return None # by default, return None
chris@160 356
chris@160 357
chris@160 358
chris@160 359 if __name__ == "__main__":
chris@160 360 main()