CurseForge.com Knowledge base

Projects / Uploading through a script

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:

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 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
Optional. 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.
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

You must login to post a comment. Don't have an account? Register to get one!

Facts

Date created
Apr 16, 2009
Last updated
Jul 12, 2011

Author