diff --git a/.gitignore b/.gitignore index a64d57b..584a579 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ packages/ /CKAN/ +/release_token.json +__pycache__/ diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 0000000..51b8260 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,63 @@ +ChangeLog: + +## Version 2.0.1 + * KSP 1.7, remove KSP max version limit + * update localization + * update MM 4.0.2 + +## Version 2.0.0 + * The plugin is excluded to the separate CommNet Antennas Info, + * Only parts are leaved there. + * either of the two 500Gm antennas cannot be a first part now, as any other mod's and stock antennas + * update MM 3.1.2 + +## Version 1.1.1.1 + * Plugin: Localized Internal AntennaType + +## Version 1.1.1 + * Plugin: Localized AntennaType + +## Version 1.1.0 (Plugin Update) + * shows modified ratings (if you have changed power modifiers in the settings) + * shows all DSN levels (Custom Barn Kit supported) + * shows DSN ratings, hightlights active DSN level, + * shows range to Built-in antenna for relays + * compact ## Version for internal antennas + +## Version 1.0.3 + * HG-25 is HG-32 now. + * When this antenna was included, the purpose was to make + * InnerSOI Commnet Network reaching Minmus with 2 of those on each vessel. + * But HG-25 doesn't make this true, because of Combinability, + * so the antenna-power was increased. + +## Version 1.0.2 + * fixed large power consumption of the HG-25 + +## Version 1.0.1 + * Recompile for KSP 1.5.1 + * fixed RelayTech One antennaType (Relay) + +## Version 1.0.0 + * Recompile for KSP 1.5.0 + * Russian localization + * Update MM to 3.1.0 + +## Version 0.9.5 + * check working with RemoteTechRedevAntennas 0.1.1 + * update desc + * add MM to archive + +## Version 0.9.3 + * fix bug in the ja-localization + +## Version 0.9.2 + * fix technodes + +## Version 0.9.1 + * remove RT art assets + +## Version 0.9 + * direct antennas: 5M and 500G + * relay antennas: 25M, 7G, 25G, 500G + * plugin for showing CombinabilityExponent in the VAB diff --git a/CommNetAntennasExtension.version b/CommNetAntennasExtension.version new file mode 100644 index 0000000..8c2e1a1 --- /dev/null +++ b/CommNetAntennasExtension.version @@ -0,0 +1,22 @@ +{ + "NAME": "CommNetAntennasExtension", + "URL": "https://raw.githubusercontent.com/yalov/CommNetAntennasExtension/master/CommNetAntennasExtension.version", + "DOWNLOAD": "https://github.com/yalov/CommNetAntennasExtension/releases", + "VERSION": { + "MAJOR": 2, + "MINOR": 0, + "PATCH": 1, + "BUILD": 0 + }, + "KSP_VERSION": { + "MAJOR": 1, + "MINOR": 7, + "PATCH": 0 + }, + "KSP_VERSION_MIN": { + "MAJOR": 1, + "MINOR": 6, + "PATCH": 0 + } +} + diff --git a/GameData/CommNetAntennasExtension/ChangeLog.md b/GameData/CommNetAntennasExtension/ChangeLog.md new file mode 100644 index 0000000..51b8260 --- /dev/null +++ b/GameData/CommNetAntennasExtension/ChangeLog.md @@ -0,0 +1,63 @@ +ChangeLog: + +## Version 2.0.1 + * KSP 1.7, remove KSP max version limit + * update localization + * update MM 4.0.2 + +## Version 2.0.0 + * The plugin is excluded to the separate CommNet Antennas Info, + * Only parts are leaved there. + * either of the two 500Gm antennas cannot be a first part now, as any other mod's and stock antennas + * update MM 3.1.2 + +## Version 1.1.1.1 + * Plugin: Localized Internal AntennaType + +## Version 1.1.1 + * Plugin: Localized AntennaType + +## Version 1.1.0 (Plugin Update) + * shows modified ratings (if you have changed power modifiers in the settings) + * shows all DSN levels (Custom Barn Kit supported) + * shows DSN ratings, hightlights active DSN level, + * shows range to Built-in antenna for relays + * compact ## Version for internal antennas + +## Version 1.0.3 + * HG-25 is HG-32 now. + * When this antenna was included, the purpose was to make + * InnerSOI Commnet Network reaching Minmus with 2 of those on each vessel. + * But HG-25 doesn't make this true, because of Combinability, + * so the antenna-power was increased. + +## Version 1.0.2 + * fixed large power consumption of the HG-25 + +## Version 1.0.1 + * Recompile for KSP 1.5.1 + * fixed RelayTech One antennaType (Relay) + +## Version 1.0.0 + * Recompile for KSP 1.5.0 + * Russian localization + * Update MM to 3.1.0 + +## Version 0.9.5 + * check working with RemoteTechRedevAntennas 0.1.1 + * update desc + * add MM to archive + +## Version 0.9.3 + * fix bug in the ja-localization + +## Version 0.9.2 + * fix technodes + +## Version 0.9.1 + * remove RT art assets + +## Version 0.9 + * direct antennas: 5M and 500G + * relay antennas: 25M, 7G, 25G, 500G + * plugin for showing CombinabilityExponent in the VAB diff --git a/GameData/CommNetAntennasExtension/ChangeLog.txt b/GameData/CommNetAntennasExtension/ChangeLog.txt deleted file mode 100644 index 430f2d3..0000000 --- a/GameData/CommNetAntennasExtension/ChangeLog.txt +++ /dev/null @@ -1,61 +0,0 @@ -ChangeLog: - -Version 2.0.0 - The plugin is excluded to the separate CommNet Antennas Info, - Only parts are leaved there. - either of the two 500Gm antennas cannot be a first part now, as any other mod's and stock antennas - update MM 3.1.2 - -Version 1.1.1.1 - Plugin: Localized Internal AntennaType - -Version 1.1.1 - Plugin: Localized AntennaType - - -Version 1.1.0 (Plugin Update) - shows modified ratings (if you have changed power modifiers in the settings) - shows all DSN levels (Custom Barn Kit supported) - shows DSN ratings, hightlights active DSN level, - shows range to Built-in antenna for relays - compact version for internal antennas - -Version 1.0.3 - HG-25 is HG-32 now. - When this antenna was included, the purpose was to make - InnerSOI Commnet Network reaching Minmus with 2 of those on each vessel. - But HG-25 doesn't make this true, because of Combinability, - so the antenna-power was increased. - -Version 1.0.2 - fixed large power consumption of the HG-25 - -Version 1.0.1 - Recompile for KSP 1.5.1 - fixed RelayTech One antennaType (Relay) - -Version 1.0.0 - Recompile for KSP 1.5.0 - Russian localization - Update MM to 3.1.0 - -Version 0.9.5 - check working with RemoteTechRedevAntennas 0.1.1 - update desc - add MM to archive - -Version 0.9.3 - fix bug in the ja-localization - -Version 0.9.2 - fix technodes - -Version 0.9.1 - remove RT art assets - -Version 0.9 - direct antennas: 5M and 500G - relay antennas: 25M, 7G, 25G, 500G - plugin for showing CombinabilityExponent in the VAB - - diff --git a/GameData/CommNetAntennasExtension/CommNetAntennasExtension.version b/GameData/CommNetAntennasExtension/CommNetAntennasExtension.version index 39e2cff..8c2e1a1 100644 --- a/GameData/CommNetAntennasExtension/CommNetAntennasExtension.version +++ b/GameData/CommNetAntennasExtension/CommNetAntennasExtension.version @@ -5,23 +5,18 @@ "VERSION": { "MAJOR": 2, "MINOR": 0, - "PATCH": 0, + "PATCH": 1, "BUILD": 0 }, "KSP_VERSION": { "MAJOR": 1, - "MINOR": 6, + "MINOR": 7, "PATCH": 0 }, "KSP_VERSION_MIN": { "MAJOR": 1, "MINOR": 6, "PATCH": 0 - }, - "KSP_VERSION_MAX": { - "MAJOR": 1, - "MINOR": 6, - "PATCH": 9 } } diff --git a/GameData/CommNetAntennasExtension/Parts/CommDeployableAntenna/DeployableAntenna.cfg b/GameData/CommNetAntennasExtension/Parts/CommDeployableAntenna/DeployableAntenna.cfg index 0e32ecf..34e8b7c 100644 --- a/GameData/CommNetAntennasExtension/Parts/CommDeployableAntenna/DeployableAntenna.cfg +++ b/GameData/CommNetAntennasExtension/Parts/CommDeployableAntenna/DeployableAntenna.cfg @@ -17,7 +17,7 @@ PART category = Communication subcategory = 0 title = #CAE_DeployableAntenna_title // C2+ High Gain Antenna HG-25 // HG-20 High Gain Antenna - manufacturer = Ionic Symphonic Protonic Electronics + manufacturer = #CAE_manufacturer // CommNet Antennas Extension description = #CAE_DeployableAntenna_desc tags = CAE_DeployableAntenna_tags attachRules = 0,1,0,0,1 diff --git a/GameData/CommNetAntennasExtension/Parts/RTantennas-patch.cfg b/GameData/CommNetAntennasExtension/Parts/RTantennas-patch.cfg index fc9d6fd..f6bd6c6 100644 --- a/GameData/CommNetAntennasExtension/Parts/RTantennas-patch.cfg +++ b/GameData/CommNetAntennasExtension/Parts/RTantennas-patch.cfg @@ -8,6 +8,7 @@ @title = #CAE_CommAntenna_title // C2 Communotron EXP-VR-2T // CommTech EXP-VR-2T @description = #CAE_CommAntenna_desc %tags = #CAE_CommAntenna_tags + %manufacturer = #CAE_manufacturer @mass = 0.02 @crashTolerance = 7 @maxTemp = 2000 @@ -34,6 +35,7 @@ @title = #CAE_CommDish1_title // C3+ Relay Antenna RA-7 //Reflectron KR-7 @description = #CAE_CommDish1_desc %tags = #CAE_CommDish1_tags + %manufacturer = #CAE_manufacturer @mass = 0.21 @crashTolerance = 8 @maxTemp = 2000 @@ -59,6 +61,7 @@ @title = #CAE_CommDish2_title // C4+ Relay Antenna RA-25 //Reflectron KR-14 @description = #CAE_CommDish2_desc %tags = #CAE_CommDish2_tags + %manufacturer = #CAE_manufacturer @mass = 0.4 @crashTolerance = 7 @maxTemp = 2000 @@ -84,6 +87,7 @@ @title = #CAE_CommDish3_title // C5+ Communotron "Tigger" // Reflectron GX-128 @description = #CAE_CommDish3_desc %tags = #CAE_CommDish3_tags + %manufacturer = #CAE_manufacturer %attachRules = 1,1,0,0,1 @mass = 0.5 @crashTolerance = 7 @@ -110,6 +114,7 @@ @title = #CAE_CommDish4_title // C5+ RelayTech One // CommTech-1 @description = #CAE_CommDish4_desc %tags = #CAE_CommDish4_tags + %manufacturer = #CAE_manufacturer %attachRules = 1,1,0,0,1 @mass = 1.5 @crashTolerance = 8 diff --git a/GameData/CommNetAntennasExtension/localization/en-us.cfg b/GameData/CommNetAntennasExtension/localization/en-us.cfg index 32fbe80..637f9c2 100644 --- a/GameData/CommNetAntennasExtension/localization/en-us.cfg +++ b/GameData/CommNetAntennasExtension/localization/en-us.cfg @@ -3,6 +3,8 @@ en-us { + #CAE_manufacturer = CommNet Antennas Extension + #CAE_CommAntenna_title = C2 Communotron EXP-VR-2T #CAE_CommAntenna_desc = This effective and compact folding antenna is highly recommended for your research missions. #CAE_CommAntenna_tags = aerial antenna deploy direct extend fold radio signal transmi diff --git a/GameData/CommNetAntennasExtension/localization/ru.cfg b/GameData/CommNetAntennasExtension/localization/ru.cfg index dd126fe..29c93d7 100644 --- a/GameData/CommNetAntennasExtension/localization/ru.cfg +++ b/GameData/CommNetAntennasExtension/localization/ru.cfg @@ -2,6 +2,7 @@ { ru { + #CAE_manufacturer = CommNet Antennas Extension #CAE_CommAntenna_title = Коммунотрон EXP-VR-2T #CAE_CommAntenna_desc = "Эта эффективная и компактная раскрывающаяся антенна крайне рекомендуется для ваших исследовательных миссий. diff --git a/GameData/ModuleManager.3.1.2.dll b/GameData/ModuleManager.3.1.2.dll deleted file mode 100644 index 26f2593..0000000 Binary files a/GameData/ModuleManager.3.1.2.dll and /dev/null differ diff --git a/GameData/ModuleManager.4.0.2.dll b/GameData/ModuleManager.4.0.2.dll new file mode 100644 index 0000000..3ef00da Binary files /dev/null and b/GameData/ModuleManager.4.0.2.dll differ diff --git a/release.bat b/release.bat deleted file mode 100644 index d18e1d6..0000000 --- a/release.bat +++ /dev/null @@ -1,37 +0,0 @@ -@echo off - -set MODNAME=CommNetAntennasExtension -set MODFOLDER=CommNetAntennasExtension -set VERSIONFILE=GameData\%MODFOLDER%\%MODNAME%.version -set RELEASESDIR=Releases -set ZIP="c:\Program Files\7-zip\7z.exe" - -REM The following requires the JQ program, available here: https://stedolan.github.io/jq/download/ -set JD=c:\Users\User\Games\Development\jq-win64 - -copy %VERSIONFILE% tmp.version -set VERSIONFILE=tmp.version - -%JD% ".VERSION.MAJOR" %VERSIONFILE% >tmpfile -set /P major=tmpfile -set /P minor=tmpfile -set /P patch=tmpfile -set /P build=.+?)\n({0}|\n\Z|\Z)".format(version) + desc = re.search(pattern, changelog, re.DOTALL).group('last') + return desc + + +def publish_to_github(token, mod_name, version, last_change, is_draft, is_prerelease, zip_file): + """create tag and publish a release""" + sys.stdout.write(" * Github connection...") + github = None + repo = None + user = None + + try: + github = Github(token) + user = github.get_user() + except Exception: + print(" failed.") + sys.exit(-1) + + try: + repo = user.get_repo(mod_name) + print(" user: {}, repo: {}".format(user.login, repo.name)) + except Exception: + print(" failed to get {}".format(mod_name)) + sys.exit(-1) + + tags = repo.get_tags() + count = tags.totalCount + + if count == 0 or (count > 0 and tags[0].name != version): + print(" * getting last commit sha...") + sha = repo.get_commits()[0].sha + + try: + repo.create_git_ref('refs/tags/{}'.format(version), sha) + except GithubException: + print(" * could not create tag on the repo.") + else: + print(" * the tag "+version+" is found, skiping...") + + rel = repo.create_git_release(tag=version, name="Version " + version, + message=last_change, + draft=is_draft, prerelease=is_prerelease) + + print(" * uploading asset...") + rel.upload_asset(path=zip_file, content_type="application/zip") + print(" * success.") + + +if __name__ == '__main__': + + jsn = json.load(open("release.json")) + tkn = json.load(open("release_token.json")) + + MODNAME = jsn["MODNAME"] + MODFOLDER = jsn["MODFOLDER"] + VERSIONFILE = jsn["VERSIONFILE"] + RELEASESDIR = jsn["RELEASESDIR"] + CHANGELOG = jsn["CHANGELOG"] + DRAFT = jsn["DRAFT"] + PRERELEASE = jsn["PRERELEASE"] + SD_ID = jsn["SPACEDOCK_ID"] + + TOKEN = tkn["GITHUB_TOKEN"] + SD_LOGIN = tkn["SPACEDOCK_LOGIN"] + SD_PASS = tkn["SPACEDOCK_PASS"] + + if MODNAME == "auto": + parent = os.path.basename([x for x in sys.path if x][0]) + onlyversionfiles = [f for f in os.listdir() if os.path.splitext(f)[1] == ".version"] + if (len(onlyversionfiles) == 1 and parent == os.path.splitext(onlyversionfiles[0])[0]): + MODNAME = parent + else: + print("Failed. You need to set up MODNAME in the release.json manually.") + input("Press Enter to exit") + sys.exit(-1) + + if MODFOLDER == "auto": + MODFOLDER = MODNAME + + if VERSIONFILE == "auto": + VERSIONFILE = MODNAME + ".version" + + copy(VERSIONFILE, "GameData/" + MODFOLDER) + copy(CHANGELOG, "GameData/" + MODFOLDER) + + VERSION = get_version(VERSIONFILE) + KSP_VER = get_version(VERSIONFILE, "KSP_VERSION") + KSP_MIN = get_version(VERSIONFILE, "KSP_VERSION_MIN") + KSP_MAX = get_version(VERSIONFILE, "KSP_VERSION_MAX") + + print("name: {}".format(MODNAME)) + print("version: {}\nksp_ver: {}\nksp_min: {}\nksp_max: {}\n" + .format(VERSION, KSP_VER, KSP_MIN, KSP_MAX)) + print("draft: {}\nprerelease: {}\n".format(DRAFT, PRERELEASE)) + print("parsing "+ CHANGELOG +" ...") + LAST_CHANGE = get_description(CHANGELOG) + print("- start of desc ------------") + print(LAST_CHANGE) + print("- end of desc --------------") + print("") + + ZIPFILE = os.path.join(RELEASESDIR, MODNAME + "-v" + VERSION + ".zip") + if os.path.exists(ZIPFILE): + print(ZIPFILE + " already exists.") + if input("Re-zip? [y/N]: ") == 'y': + archive_to(ZIPFILE) + else: + print("Creating "+ ZIPFILE +" ...") + archive_to(ZIPFILE) + + + print("") + print("GITHUB:") + print("You already push your changes to a remote repo, don't you?") + print("Create the tag, and publish a {}{} with the asset?".format("DRAFT " if DRAFT else "","PRERELEASE" if PRERELEASE else "RELEASE" )) + if input("[y/N]: ") == 'y': + publish_to_github(TOKEN, MODNAME, VERSION, LAST_CHANGE, DRAFT, PRERELEASE, ZIPFILE) + + # ====================================== + + print("") + print("SPACEDOCK:") + if not SD_ID: + print("Spacedock number is not found in the json.") + input("Press Enter to exit") + sys.exit(-1) + + print("Accessing to Spacedock...") + all_versions = [v['name'] for v in GetSpacedockKSPVersions()] + + if not all_versions: + print("Failed. Could not access to Spacedock.") + input("Press Enter to exit") + sys.exit(-1) + + if KSP_VER not in all_versions: + print("KSP {} is not supported by Spacedock,\nlast supported version is KSP {}" + .format(KSP_VER, all_versions[0])) + input("Press Enter to exit") + sys.exit(-1) + + print("KSP {} is supported by Spacedock.".format(KSP_VER)) + + mod_details = GetSpacedockModDetails(SD_ID) + + if not mod_details or 'error' in mod_details: + print("The mod #{} isn't found.".format(SD_ID)) + input("Press Enter to exit") + sys.exit(-1) + + print("Spacedock info:\nID: {}, NAME: {}\nLast Release {} (KSP {})".format( + SD_ID, mod_details['name'], + mod_details['versions'][0]['friendly_version'], + mod_details['versions'][0]['game_version'] + )) + + print("Publish {} (KSP {}) to the Spacedock?".format(VERSION, KSP_VER)) + if input("[y/N]: ") == 'y': + PublishToSpacedock(SD_ID, ZIPFILE, LAST_CHANGE, VERSION, KSP_VER, SD_LOGIN, SD_PASS) + + input("Press Enter to exit") + sys.exit(0) diff --git a/release_spacedock_utils.py b/release_spacedock_utils.py new file mode 100644 index 0000000..5627d4f --- /dev/null +++ b/release_spacedock_utils.py @@ -0,0 +1,229 @@ +"""utils for accessing to the spacedock""" +# Public domain license. +# Based on: https://github.com/ihsoft/KSPDev_ReleaseBuilder +# $version: 2 +# -*- coding: utf-8 -*- + +import json +import urllib.request +import urllib.error +import urllib.parse + +import io +import random +import string +import ntpath + + +# Endpoint for all the API requests +API_BASE_URL = 'https://spacedock.info' + +# The actions paths. +API_AUTHORIZE = '/api/login' +API_UPDATE_MOD_TMPL = '/api/mod/{mod_id}/update' +API_GET_VERSIONS = '/api/kspversions' +API_GET_MOD = '/api/mod/{mod_id}' + +# The authorization cookie. It's only created once. To refresh it, simply +# set it to None. +authorized_cookie = None + + +def PublishToSpacedock(mod_id, filepath, changelog, mod_version, game_version, login, password): + """ + Args: + mod_id: The mod ID to update. + filepath: A full or relative path to the local file. + changelog: The change log content. + mod_version: The version of the mod being published. + game_version: The KSP version to publish for. + Returns: + The response object. + """ + + headers, data = EncodeFormData([ + {'name': 'version', 'data': mod_version}, + {'name': 'changelog', 'data': changelog}, + {'name': 'game-version', 'data': game_version}, + {'name': 'notify-followers', 'data': 'yes'}, + {'name': 'zipball', 'filename': filepath}, + ]) + url, headers = _GetAuthorizedEndpoint( + API_UPDATE_MOD_TMPL, headers, login, password, mod_id=mod_id) + + resp = _CallAPI(url, data=data, headers=headers) + + print('success.') + print('The new version is now available and the followers are notified!') + + return resp + + +def GetSpacedockModDetails(mod_id): + """Gets the mod informnation. + This call does NOT require authorization. + Args: + mod_id: The mod to request. + Returns: + The response object. + """ + url = _MakeAPIUrl(API_GET_MOD, mod_id=mod_id) + response_obj, _ = _CallAPI(url, None, None) + return response_obj + + +def GetSpacedockKSPVersions(): + """Gets the available versions of the game. + This call does NOT require authorization. + Returns: + A list of objects: { 'name': , 'id': } + """ + response = _CallAPI(_MakeAPIUrl(API_GET_VERSIONS), None, None) + versions = [{'name': x['friendly_version'], 'id': x['id']} for x in response[0]] + + return versions + + +def _MakeAPIUrl(action_path, **kwargs): + """Makes a URL for the action.""" + return API_BASE_URL + action_path.format(**kwargs) + + +def _CallAPI(url, data, headers, raise_on_error=True): + """Invokes the API call.""" + resp_obj = {'error': True, 'reason': 'unknown'} + try: + request = urllib.request.Request(url, data, headers=headers or {}) + response = urllib.request.urlopen(request) + resp_obj = json.loads(response.read()) + headers = response.info() + except urllib.error.HTTPError as ex: + resp_obj = {'error': True, 'reason': '%d - %s' % (ex.code, ex.reason)} + try: + resp_obj = json.loads(ex.read()) + except Exception: + pass # Not a JSON response + if ex.code == 401: + print("AuthorizationRequiredError") + + if type(resp_obj) is dict and resp_obj.get('error'): + if raise_on_error: + print("BadResponseError") + return resp_obj, None + return resp_obj, headers + + +def _GetAuthorizedEndpoint(api_path, headers, login, password, **kwargs): + """Gets API URL and the authorization headers. + + The login/password must be set in the global variables API_LOGIN/API_PASS. + """ + global authorized_cookie + + url = _MakeAPIUrl(api_path, **kwargs) + if not headers: + headers = {} + + if not authorized_cookie: + if not login or not password: + print("BadCredentialsError: API_LOGIN and/or API_PASS not set") + exit(0) + + auth_headers, data = EncodeFormData([ + {'name': 'username', 'data': login}, + {'name': 'password', 'data': password}, + ]) + resp, auth_headers = _CallAPI( + API_BASE_URL + API_AUTHORIZE, data, auth_headers, + raise_on_error=False) + if resp['error']: + print("BadCredentialsError") + exit(0) + authorized_cookie = auth_headers['set-cookie'] + + headers['Cookie'] = authorized_cookie + return url, headers + + +# Utils: Provides helpers to deal with the multipart/form-data MIME types. + + +def EncodeFormData(fields): + """Encodes the provided arguments as the web form fields. + + The argument must be an array of objects that define the fields to be passed: + - 'name': A required property than defines the name of the web form field. + - 'data': The raw data to pass. If it's of type 'string', then it's passed as + 'text/plain'. Otherwise, it's serialized as JSON and passed as + 'application/json'. + - 'filename': An optional property that designates a path to the binary file + to tansfer. The 'data' field is ignored in this case, and the real data is + read from the file. The data is passed as 'application/octet-stream', and + server will receive the file name part of the path. The part can be + relative, in which case it's counted realtive to the main module, or it can + be absolute. + + Example: + [ + { 'name': 'metadata', 'data': metadatObj }, + { 'name': 'file', 'filename': '/home/me/releases/MyMod_v1.0.zip' } + ] + + @param fields: An array of the fields to encode. + """ + boundry = '----WebKitFormBoundary' + IdGenerator(16) + + data_stream = io.BytesIO() + for field in fields: + filename = field.get('filename') + if filename: + with open(filename, 'rb') as f: + data = f.read() + content_type = 'application/octet-stream' + filename = ntpath.basename(filename) + else: + data = field['data'] + if type(data) != str: + content_type = 'application/json' + data = json.dumps(data) + else: + content_type = 'text/plain; charset=utf-8' + data = data.encode('utf-8') + _WriteFormData(data_stream, boundry, + field['name'], content_type, data, filename) + + data_stream.write(b'--') + data_stream.write(boundry.encode()) + data_stream.write(b'--') + data_stream.write(b'\r\n') + + headers = { + 'Content-Type': 'multipart/form-data; boundary=%s' % boundry, + 'Content-Length': str(data_stream.tell()), + } + return headers, data_stream.getvalue() + + +def IdGenerator(size, chars=string.ascii_letters + string.digits): + """Makes a unique random string of the requested size.""" + return ''.join(random.choice(chars) for _ in range(size)) + + +def _WriteFormData(stream, boundry, name, content_type, data, filename=None): + """Helper method to write a single web form field.""" + stream.write(b'--') + stream.write(boundry.encode()) + stream.write(b'\r\n') + + if filename: + stream.write(('Content-Disposition: form-data; name="%s"; filename="%s"' % + (name, filename)).encode()) + else: + stream.write(('Content-Disposition: form-data; name="%s"' % + (name)).encode()) + stream.write(b'\r\n') + stream.write(('Content-Type: %s' % content_type).encode()) + stream.write(b'\r\n') + stream.write(b'\r\n') + stream.write(data) + stream.write(b'\r\n')