If you want to automate your uploading process through CurseForge, there is a way to do this properly:
Getting your API Key
First, you must go to http://www.curseforge.com/home/api-key/ and generate an API key if you have not already. This is unique to you and provides you a way to authenticate through scripts without a complex login process.
Figuring out your game versions
Next, for the game you want, you're going to want to calculate what game versions you support, knowing the backend ID. Thankfully, we provide an API call for you:
- World of Warcraft: http://wow.curseforge.com/game-versions.json
- Warhammer: http://war.curseforge.com/game-versions.json
- Runes of Magic: http://rom.curseforge.com/game-versions.json
And so forth, changing the domain based on the game you want. This will return a JSON object of the following form:
{"1": // this is the id of the game version {"is_development": false, // true if it's in PTR but not officially released "breaks_compatibility": false, // change between, e.g. 2.4 and 3.0, but not 3.0 to 3.1 "release_date": "2006-09-26", // "YYYY-MM-DD" or null "name": "1.12.0", // name of the version "internal_id": "11200"}} // if available, an internal version. For WoW, this is the TOC number. null if not available
You need to calculate 1-3 IDs of game versions that the file is compatible with.
Actually uploading
Now to upload your file.
Find your upload file page, e.g. http://wow.curseforge.com/projects/my-project/upload-file/. Change the final slash to .json, and you have http://wow.curseforge.com/projects/quest-helper/upload-file.json
Now you make a POST request to that URL with the following url-encoded params:
- name
- The name of the file you're uploading, this should be the version's name, do not include your project's name.
- game_version
- Specify 1-3 times with the IDs you attained previously.
- file_type
- Specify
afor Alpha,bfor Beta, andrfor Release. - change_log
- The change log of the file. Up to 50k characters is acceptable.
- change_markup_type
- Markup type for your change log.
creoleorplainis recommended. - known_caveats
- Optional. The known caveats of the file. Up to 50k characters is acceptable.
- caveats_markup_type
- Markup type for your known caveats.
creoleorplainis recommended. - file
- The actual zip file for your addon.
There are a few results of your request:
- 403 Forbidden
- You didn't specify your API Key correctly or you do not have permission to upload a file to that project.
- 404 Not Found
- Project couldn't be found. You either specified it wrong or it was renamed.
- 405 Method Not Allowed
- You did a GET instead of a POST.
- 422 Unprocessable Entity
- You have an error in your form. This is a JSON response that will tell you which fields had an issue.
- 201 Created
- Hurrah, your file is uploaded properly.
Example code in python
#!/usr/bin/env python from httplib import HTTPConnection from os.path import basename, exists from mimetools import choose_boundary try: import simplejson as json except ImportError: import json def get_game_versions(game): """ Return the JSON response as given from /game-versions.json from curseforge.com of the given game `game` The shortened version of the game, e.g. "wow", "war", or "rom" """ conn = HTTPConnection('%(game)s.curseforge.com' % { 'game': game }) conn.request("GET", '/game-versions.json') response = conn.getresponse() assert response.status == 200, "%(status)d %(reason)s from /game-versions.json" % { 'status': response.status, 'reason': response.reason } assert response.content_type == 'application/json' data = json.loads(response.read()) return data def upload_file(api_key, game, project_slug, name, game_version_ids, file_type, change_log, change_markup_type, known_caveats, caveats_markup_type, filepath): """ Upload a file to CurseForge.com on your project `api_key` The api-key from http://www.curseforge.com/home/api-key/ `game` The shortened version of the game, e.g. "wow", "war", or "rom" `project_slug` The slug of your project, e.g. "my-project" `name` The name of the file you're uploading, this should be the version's name, do not include your project's name. `game_version_ids` A set of game version ids. `file_type` Specify 'a' for Alpha, 'b' for Beta, and 'r' for Release. `change_log` The change log of the file. Up to 50k characters is acceptable. `change_markup_type` Markup type for your change log. creole or plain is recommended. `known_caveats` The known caveats of the file. Up to 50k characters is acceptable. `caveats_markup_type` Markup type for your known caveats. creole or plain is recommended. `filepath` The path to the file to upload. """ assert len(api_key) == 40 assert 1 <= len(game_version_ids) <= 3 assert file_type in ('r', 'b', 'a') assert exists(filepath) params = [] params.append(('name', name)) for game_version_id in game_version_ids: params.append(('game_version', game_version_id)) params.append(('file_type', file_type)) params.append(('change_log', change_log)) params.append(('change_markup_type', change_markup_type)) params.append(('known_caveats', known_caveats)) params.append(('caveats_markup_type', caveats_markup_type)) content_type, body = encode_multipart_formdata(params, [('file', filepath)]) headers = { "User-Agent": "CurseForge Uploader Script/1.0", "Content-type": content_type, "X-API-Key": api_key} conn = HTTPConnection('%(game)s.curseforge.com' % { 'game': game }) conn.request("POST", '/projects/%(slug)s/upload-file.json' % {'slug': project_slug}, body, headers) response = conn.getresponse() if response.status == 201: print "Successfully uploaded %(name)s" % { 'name': name } elif response.status == 422: assert response.content_type == 'application/json' errors = json.loads(response.read()) print "Form error with uploading %(name)s:" % { 'name': name } for k, items in errors.iteritems(): for item in items: print " %(k)s: %(item)s" % { 'k': k, 'name': name } else: print "Error with uploading %(name)s: %(status)d %(reason)s" % { 'name': name, 'status': response.status, 'reason': response.reason } def encode_multipart_formdata(fields, files): """ Encode data in multipart/form-data format. `fields` A sequence of (name, value) elements for regular form fields. `files` A sequence of (name, filename) elements for data to be uploaded as files Return (content_type, body) ready for httplib.HTTP instance """ boundary = choose_boundary() L = [] for key, value in fields: if value is None: value = '' elif value is False: continue L.append('--%(boundary)s' % {'boundary': boundary}) L.append('Content-Disposition: form-data; name="%(name)s"' % {'name': key}) L.append('') L.append(str(value)) for key, filename in files: f = file(filename, 'rb') filedata = f.read() f.close() L.append('--%(boundary)s' % {'boundary': boundary}) L.append('Content-Disposition: form-data; name="%(name)s"; filename="%(filename)s"' % { 'name': key, 'filename': basename(filename) }) L.append('Content-Type: application/zip') L.append('') L.append(filedata) L.append('--%(boundary)s--' % {'boundary': boundary}) L.append('') body = '\r\n'.join(L) content_type = 'multipart/form-data; boundary=%(boundary)s' % { 'boundary': boundary } return content_type, body
- 2 comments
- 2 comments
Facts
- Date created
- Apr 16, 2009
- Last updated
- Jul 12, 2011
- Reply
- #2
feildmaster Jan 19, 2013 at 07:44 UTC - 0 likesThe response wouldn't happen to include the URL or ID to the uploaded file page would it?
Would it be possible to include such a feature? (To automatically upload, and then provide a link if you wish to view it)
Plugins | SimpleNotice, bringing plugins and clients together

- Reply
- #1
Jaliborc Jan 06, 2012 at 16:37 UTC - 0 likesUsing Requests I was able to make a much simpler script in python: https://gist.github.com/1571919
Developer of addons such as Bagnon, PetTracker and OmniCC
Visit me at jaliborc.com.