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!

  • 8 comments
  • Avatar of wiki65 wiki65 Aug 19, 2014 at 05:33 UTC - 0 likes

    Game spot is a gambling site to provide announcement, reviews, previews, downloading, as well as other information on selected games. The web page was launched. best Moroccan argan oil for hair best vitamin c serum uses best seo company in phoenix upprcut.com san francisco seo company upprcut.com top web hosting review sites

  • Avatar of wiki65 wiki65 Aug 19, 2014 at 05:30 UTC - 0 likes

    Your own game may be the sole via will certainly Young's business record fridays little one. It had been co written through younger, taio johnson in addition to blair mackichan. The item went along to amount a few in the uk singles graph. top web hosting review sites

  • Avatar of rossyrose rossyrose Aug 13, 2014 at 03:41 UTC - 0 likes

    It's extremely comprehensible and exceedingly wise. You have even figured out how to make it reasonable and simple to peruse. You have some genuine composition ability. Much thanks to you. where to buy soundcloud followers

  • Avatar of robotbrain robotbrain Jan 28, 2014 at 21:20 UTC - 0 likes

    Now one question: how do I get a download link for the uploaded file automatically? Oh whoops it has a location header in the response

    Last edited Jan 28, 2014 by robotbrain
  • Avatar of storm345 storm345 Jan 27, 2014 at 17:52 UTC - 0 likes

    Hey, I use the API in my Bukkit Plugin, UltimatePluginUpdater, to update all server plugins automatically. However, recently I have been noticing that your service is more frequently returning error 504 and is generally taking longer to respond to requests. I presume this is a result of the service being more used; but is there anything that can be done to make it go faster? (This never used to be an issue!) Thanks :)

    -storm345

  • Avatar of Udorn Udorn Sep 14, 2013 at 10:02 UTC - 0 likes

    I was using the python script a while, but from one day to the next my python installation was broken and the script didn't function anymore. So I wrote a shell script (may be used on windows with cygwin), which may be helpful for other people as well:

    #!/bin/bash
    
    FILE_TYPE="$1"
    NAME="$2"
    KEY="$3"
    ZIP_FILE="$4"
    CHANGES_FILE="$5"
    
    API_KEY="Fill in your API key here"
    GAME_VERSIONS="322"
    CHANGE_MARKUP_TYPE="creole"
    KNOWN_CAVEATS=""
    CAVEATS_MARKUP_TYPE="plain"
    
    curl -F "name=$NAME" -F "game_versions=$GAME_VERSIONS" -F "file_type=$FILE_TYPE" \
        -F "change_log=<$CHANGES_FILE" -F "change_markup_type=$CHANGE_MARKUP_TYPE" \
        -F "known_caveats=$KNOWN_CAVEATS" -F "caveats_markup_type=$CAVEATS_MARKUP_TYPE" \
        -F "file=@$ZIP_FILE" -H "X-API-Key: $API_KEY" "http://wow.curseforge.com/addons/$KEY/upload-file.json"
    

    For my Addon AuctionMaster the call would be:

    upload.sh  r 5.6.0 AuctionMaster ./AuctionMaster-5.6.0.zip ./Changes.txt
    

    This may be combinded with another script that does all the subversion, toc editing and zip-build stuff.

  • Avatar of feildmaster feildmaster Jan 19, 2013 at 07:44 UTC - 0 likes

    The 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)

  • Avatar of Jaliborc Jaliborc Jan 06, 2012 at 16:37 UTC - 0 likes

    Using Requests I was able to make a much simpler script in python: https://gist.github.com/1571919

    Last edited Jan 06, 2012 by Jaliborc

    Developer of addons such as Bagnon, PetTracker and OmniCC
    Visit me at jaliborc.com.

  • 8 comments

Facts

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

Author