chris@160: #!/usr/bin/python chris@160: chris@160: # Copyright 2015 Section6. All Rights Reserved. chris@160: chris@160: import argparse chris@160: import getpass chris@160: import json chris@160: import os chris@160: import requests chris@160: import shutil chris@160: import stat chris@160: import tempfile chris@160: import time chris@160: import urlparse chris@160: import zipfile chris@160: import sys chris@160: chris@160: class Colours: chris@160: purple = "\033[95m" chris@160: cyan = "\033[96m" chris@160: dark_cyan = "\033[36m" chris@160: blue = "\033[94m" chris@160: green = "\033[92m" chris@160: yellow = "\033[93m" chris@160: red = "\033[91m" chris@160: bold = "\033[1m" chris@160: underline = "\033[4m" chris@160: end = "\033[0m" chris@160: chris@160: def __zip_dir(in_dir, zip_path, file_filter=None): chris@160: zf = zipfile.ZipFile(zip_path, mode="w", compression=zipfile.ZIP_DEFLATED) chris@160: for subdir, dirs, files in os.walk(in_dir): chris@160: for file in files: chris@160: if (file_filter is None) or (len(file_filter) > 0 and file.lower().split(".")[-1] in file_filter): chris@160: zf.write( chris@160: filename=os.path.join(subdir,file), chris@160: arcname=os.path.relpath(os.path.join(subdir,file), start=in_dir)) chris@160: return zip_path chris@160: chris@160: def __unzip(zip_path, target_dir): chris@160: """Unzip a file to a given directory. All destination files are overwritten. chris@160: """ chris@160: zipfile.ZipFile(zip_path).extractall(target_dir) chris@160: chris@160: def main(): chris@160: parser = argparse.ArgumentParser( chris@160: description="Compiles a Pure Data file.") chris@160: parser.add_argument( chris@160: "input_dir", chris@160: help="A directory containing _main.pd. The entire directory will be uploaded.") chris@160: parser.add_argument( chris@160: "-n", "--name", chris@160: default="heavy", chris@160: help="Patch name. If it doesn't exist, the uploader will fail. Make sure that it exists on the Heavy website.") chris@160: parser.add_argument( chris@160: "-g", "--gen", chris@160: nargs="+", chris@160: default=["c"], chris@160: help="List of generator outputs. Currently supported generators are 'c' and 'js'.") chris@160: parser.add_argument( chris@160: "-b", chris@160: help="All files will be placed in the output directory, placed in their own subdirectory corresonding to the generator name.", chris@160: action="count") chris@160: parser.add_argument( chris@160: "-o", "--out", chris@160: nargs="+", chris@160: default=["./"], # by default chris@160: help="List of destination directories for retrieved files. Order should be the same as for --gen.") chris@160: parser.add_argument( chris@160: "-d", "--domain", chris@160: default="https://enzienaudio.com", chris@160: help="Domain. Default is https://enzienaudio.com.") chris@160: parser.add_argument( chris@160: "-x", chris@160: help="Don't save the returned token.", chris@160: action="count") chris@160: parser.add_argument( chris@160: "-z", chris@160: help="Force the use of a password, regardless of saved token.", chris@160: action="count") chris@160: parser.add_argument( chris@160: "--noverify", chris@160: help="Don't verify the SSL connection. Generally a bad idea.", chris@160: action="count") chris@160: parser.add_argument( chris@160: "-v", "--verbose", chris@160: help="Show debugging information.", chris@160: action="count") chris@160: args = parser.parse_args() chris@160: chris@160: domain = args.domain or "https://enzienaudio.com" chris@160: chris@160: post_data = {} chris@160: chris@160: # token should be stored in ~/.heavy/token chris@160: token_path = os.path.expanduser(os.path.join("~/", ".heavy", "token")) chris@160: if os.path.exists(token_path) and not args.z: chris@160: with open(token_path, "r") as f: chris@160: post_data["credentials"] = { chris@160: "token": f.read() chris@160: } chris@160: else: chris@160: # otherwise, get the username and password chris@160: post_data["credentials"] = { chris@160: "username": raw_input("Enter username: "), chris@160: "password": getpass.getpass("Enter password: ") chris@160: } chris@160: chris@160: tick = time.time() chris@160: chris@160: # make a temporary directory chris@160: temp_dir = tempfile.mkdtemp(prefix="lroyal-") chris@160: chris@160: # zip up the pd directory into the temporary directory chris@160: try: chris@160: if not os.path.exists(os.path.join(args.input_dir, "_main.pd")): chris@160: raise Exception("Root Pd directory does not contain a file named _main.pd.") chris@160: zip_path = __zip_dir( chris@160: args.input_dir, chris@160: os.path.join(temp_dir, "archive.zip"), chris@160: file_filter={"pd"}) chris@160: except Exception as e: chris@160: print e chris@160: shutil.rmtree(temp_dir) # clean up the temporary directory chris@160: return chris@160: chris@160: post_data["name"] = args.name chris@160: chris@160: # the outputs to generate (always include c) chris@160: __SUPPORTED_GENERATOR_SET = {"c", "js"} chris@160: post_data["gen"] = list(({"c"} | set(args.gen)) & __SUPPORTED_GENERATOR_SET) chris@160: chris@160: # upload the job, get the response back chris@160: # NOTE(mhroth): multipart-encoded file can only be sent as a flat dictionary, chris@160: # but we want to send a json encoded deep dictionary. So we do a bit of a hack. chris@160: r = requests.post( chris@160: urlparse.urljoin(domain, "/a/heavy"), chris@160: data={"json":json.dumps(post_data)}, chris@160: files={"file": (os.path.basename(zip_path), open(zip_path, "rb"), "application/zip")}, chris@160: verify=False if args.noverify else True) chris@160: chris@160: if r.status_code != requests.codes.ok: chris@160: shutil.rmtree(temp_dir) # clean up the temporary directory chris@160: r.raise_for_status() # raise an exception chris@160: chris@160: # decode the JSON API response chris@160: r_json = r.json() chris@160: chris@160: """ chris@160: { chris@160: "data": { chris@160: "compileTime": 0.05078411102294922, chris@160: "id": "mhroth/asdf/Edp2G", chris@160: "slug": "Edp2G", chris@160: "index": 3, chris@160: "links": { chris@160: "files": { chris@160: "linkage": [ chris@160: { chris@160: "id": "mhroth/asdf/Edp2G/c", chris@160: "type": "file" chris@160: } chris@160: ], chris@160: "self": "https://enzienaudio.com/h/mhroth/asdf/Edp2G/files" chris@160: }, chris@160: "project": { chris@160: "linkage": { chris@160: "id": "mhroth/asdf", chris@160: "type": "project" chris@160: }, chris@160: "self": "https://enzienaudio.com/h/mhroth/asdf" chris@160: }, chris@160: "self": "https://enzienaudio.com/h/mhroth/asdf/Edp2G", chris@160: "user": { chris@160: "linkage": { chris@160: "id": "mhroth", chris@160: "type": "user" chris@160: }, chris@160: "self": "https://enzienaudio.com/h/mhroth" chris@160: } chris@160: }, chris@160: "type": "job" chris@160: }, chris@160: "included": [ chris@160: { chris@160: "filename": "file.c.zip", chris@160: "generator": "c", chris@160: "id": "mhroth/asdf/Edp2G/c", chris@160: "links": { chris@160: "self": "https://enzienaudio.com/h/mhroth/asdf/Edp2G/c/file.c.zip" chris@160: }, chris@160: "mime": "application/zip", chris@160: "type": "file" chris@160: } chris@160: ], chris@160: "warnings": [], chris@160: "meta": { chris@160: "token": "11AS0qPRmjTUHEMSovPEvzjodnzB1xaz" chris@160: } chris@160: } chris@160: """ chris@160: reply_json = r.json() chris@160: if args.verbose: chris@160: print json.dumps( chris@160: reply_json, chris@160: sort_keys=True, chris@160: indent=2, chris@160: separators=(",", ": ")) chris@160: chris@160: # update the api token, if present chris@160: if "token" in reply_json.get("meta",{}) and not args.x: chris@160: if not os.path.exists(os.path.dirname(token_path)): chris@160: os.makedirs(os.path.dirname(token_path)) # ensure that the .heavy directory exists chris@160: with open(token_path, "w") as f: chris@160: f.write(reply_json["meta"]["token"]) chris@160: os.chmod(token_path, stat.S_IRUSR | stat.S_IWUSR) # force rw------- permissions on the file chris@160: chris@160: # print any warnings chris@160: for x in r_json["warnings"]: chris@160: print "{0}Warning:{1} {2}".format(Colours.yellow, Colours.end, x["detail"]) chris@160: chris@160: # check for errors chris@160: if len(r_json.get("errors",[])) > 0: chris@160: shutil.rmtree(temp_dir) # clean up the temporary directory chris@160: for x in r_json["errors"]: chris@160: print "{0}Error:{1} {2}".format(Colours.red, Colours.end, x["detail"]) chris@160: sys.exit(1) chris@160: return chris@160: chris@160: # retrieve all requested files chris@160: for i,g in enumerate(args.gen): chris@160: file_url = __get_file_url_for_generator(reply_json, g) chris@160: if file_url is not None and (len(args.out) > i or args.b): chris@160: r = requests.get( chris@160: file_url, chris@160: cookies={"token": reply_json["meta"]["token"]}, chris@160: verify=False if args.noverify else True) chris@160: r.raise_for_status() chris@160: chris@160: # write the reply to a temporary file chris@160: c_zip_path = os.path.join(temp_dir, "archive.{0}.zip".format(g)) chris@160: with open(c_zip_path, "wb") as f: chris@160: f.write(r.content) chris@160: chris@160: # unzip the files to where they belong chris@160: if args.b: chris@160: target_dir = os.path.join(os.path.abspath(os.path.expanduser(args.out[0])), g) chris@160: else: chris@160: target_dir = os.path.abspath(os.path.expanduser(args.out[i])) chris@160: if not os.path.exists(target_dir): chris@160: os.makedirs(target_dir) # ensure that the output directory exists chris@160: __unzip(c_zip_path, target_dir) chris@160: chris@160: print "{0} files placed in {1}".format(g, target_dir) chris@160: else: chris@160: print "{0}Warning:{1} {2} files could not be retrieved.".format( chris@160: Colours.yellow, Colours.end, chris@160: g) chris@160: chris@160: # delete the temporary directory chris@160: shutil.rmtree(temp_dir) chris@160: chris@160: print "Job URL", reply_json["data"]["links"]["self"] chris@160: print "Total request time: {0}ms".format(int(1000.0*(time.time()-tick))) chris@160: chris@160: sys.exit(0) chris@160: chris@160: def __get_file_url_for_generator(json_api, g): chris@160: """Returns the file link for a specific generator. chris@160: Returns None if no link could be found. chris@160: """ chris@160: for i in json_api["included"]: chris@160: if g == i["generator"]: chris@160: return i["links"]["self"] chris@160: return None # by default, return None chris@160: chris@160: chris@160: chris@160: if __name__ == "__main__": chris@160: main()