#!/usr/bin/env python # encoding: utf8 """ add-on_manager.py -- a command-line client for the Wesnoth add-on server This tool is mainly intendended for user-made content authors and maintainers. It can be used to manage the WML content on the Wesnoth add-on server. Available functions include listing, downloading, uploading, and deleting add-ons. """ import sys, os.path, re, time, glob, shutil from subprocess import Popen import wesnoth.wmldata as wmldata import wesnoth.wmlparser as wmlparser from wesnoth.campaignserver_client import CampaignClient if __name__ == "__main__": import optparse try: import psyco psyco.full() except ImportError: pass optionparser = optparse.OptionParser() optionparser.add_option("-a", "--address", help="specify server address", default="add-ons.wesnoth.org") optionparser.add_option("--html", help="Output a HTML overview into the given directory.",) optionparser.add_option("-p", "--port", help="specify server port or BfW version (%s)" % " or ".join( map(lambda x: x[1], CampaignClient.portmap)), default=CampaignClient.portmap[0][0]) optionparser.add_option("-l", "--list", help="list available add-ons", action="store_true",) optionparser.add_option("-w", "--wml", help="when listing add-ons, list the raw wml", action="store_true",) optionparser.add_option("-C", "--color", help="use colored WML output", action="store_true",) optionparser.add_option("-c", "--campaigns-dir", help="directory where add-ons are stored", default=".") optionparser.add_option("-P", "--password", help="password to use") optionparser.add_option("-d", "--download", help="download the named add-on; " + "name may be a Python regexp matched against all add-on names " + "(specify the path where to put it with -c, " + "current directory will be used by default)") optionparser.add_option("-T", "--type", help="Type of addons to download, e.g. 'era' or 'campaign'.") optionparser.add_option("-t", "--tar", help="When used together with --download, create tarballs of any " + "downloaded addons and put into the specified directory.") optionparser.add_option("--pbl", help="override standard PBL location") optionparser.add_option("-u", "--upload", help="Upload an add-on. " + "UPLOAD should be either the name of an add-on subdirectory," + "(in which case the client looks for _server.pbl beneath it) " + "or a path to the .pbl file (in which case the name of the " + "add-on subdirectory is the name of the path with .pbl removed)") optionparser.add_option("-s", "--status", help="Display the status of addons installed in the given " + "directory.") optionparser.add_option("-f", "--update", help="Update all installed add-ons in the given directory. " + "This works by comparing the _info.cfg file in each addon directory " + "with the version on the server.") optionparser.add_option("-V", "--verbose", help="be even more verbose for everything", action="store_true",) optionparser.add_option("-r", "--remove", help="remove the named add-on from the server, " + "set the password -P") optionparser.add_option("-R", "--raw-download", action="store_true", help="download as a binary WML packet") optionparser.add_option("--url", help="When used with --html, " + "a download link will be added for each campaign, with the given " + "base URL.") optionparser.add_option("-U", "--unpack", help="unpack the file UNPACK as a binary WML packet " + "(specify the add-on path with -c)") optionparser.add_option("--change-passphrase", nargs=3, metavar="ADD-ON OLD NEW", help="Change the passphrase for ADD-ON from OLD to NEW") options, args = optionparser.parse_args() port = options.port if "." in options.port: for (portnum, version) in CampaignClient.portmap: if options.port == version: port = portnum break else: sys.stderr.write("Unknown BfW version %s\n" % options.port) sys.exit(1) address = options.address if not ":" in address: address += ":" + str(port) def get(name, version, uploads, dependencies, cdir): mythread = cs.get_campaign_raw_async(name) pcounter = 0 while not mythread.event.isSet(): mythread.event.wait(1) if pcounter != cs.counter: print "%s: %d/%d" % (name, cs.counter, cs.length) pcounter = cs.counter if options.raw_download: file(name, "w").write(mythread.data) else: decoded = cs.decode(mythread.data) dirname = os.path.join(cdir, name) oldcfg_path = os.path.join(cdir, name + ".cfg") # Try to remove old campaign in case it exists. shutil.rmtree(dirname, True) try: os.remove(oldcfg_path) except OSError: pass print "Unpacking %s..." % name cs.unpackdir(decoded, cdir, verbose=options.verbose) info = os.path.join(dirname, "_info.cfg") try: f = file(info, "w") infowml = """[info] version="%s" uploads="%s" dependencies="%s" [/info]""" f.write(infowml % (version, uploads, dependencies)) f.close() except OSError: pass for message in decoded.find_all("message", "error"): print message.get_text_val("message") if options.tar: try: os.mkdir(options.tar) except OSError: pass tarname = options.tar + "/" + name + ".tar.bz2" if os.path.isfile(oldcfg_path): oldcfg = name + ".cfg" if options.verbose: sys.stderr.write("Creating tarball with command: tar " + "cjf %(tarname)s -C %(cdir)s %(name)s %(oldcfg)s\n" % locals()) Popen(["tar", "cjf", tarname, "-C", cdir, name, oldcfg]) else: if options.verbose: sys.stderr.write("Creating tarball with command: tar " + "cjf %(tarname)s -C %(cdir)s %(name)s\n" % locals()) Popen(["tar", "cjf", tarname, "-C", cdir, name]) def get_info(name): """ Get info for a locally installed add-on. It expects a direct path to the _info.cfg file. """ if not os.path.exists(name): return None, None p = wmlparser.Parser(None) p.parse_file(name) info = wmldata.DataSub("WML") p.parse_top(info) uploads = info.get_or_create_sub("info").get_text_val("uploads", "") version = info.get_or_create_sub("info").get_text_val("version", "") return uploads, version campaign_list = None if options.list: cs = CampaignClient(address) campaign_list = data = cs.list_campaigns() if data: campaigns = data.get_or_create_sub("campaigns") if options.wml: for campaign in campaigns.get_all("campaign"): campaign.debug(show_contents=True, use_color=options.color) else: column_sizes = [10, 5, 10, 7, 8, 8, 10, 5, 10, 13] columns = [["type", "name", "title", "author", "version", "uploads", "downloads", "size", "timestamp", "translate"]] for campaign in campaigns.get_all("campaign"): column = [ campaign.get_text_val("type", "?"), campaign.get_text_val("name", "?"), campaign.get_text_val("title", "?"), campaign.get_text_val("author", "?"), campaign.get_text_val("version", "?"), campaign.get_text_val("uploads", "?"), campaign.get_text_val("downloads", "?"), campaign.get_text_val("size", "?"), time.ctime(int(campaign.get_text_val("timestamp", "0"))), campaign.get_text_val("translate", "?")] columns.append(column) for i, s in enumerate(column_sizes): if 1 + len(column[i]) > s: column_sizes[i] = 1 + len(column[i]) for c in columns: for i, f in enumerate(c): sys.stdout.write(f.ljust(column_sizes[i])) sys.stdout.write("\n") for message in data.find_all("message", "error"): print message.get_text_val("message") else: sys.stderr.write("Could not connect.\n") elif options.download: cs = CampaignClient(address) fetchlist = [] campaign_list = data = cs.list_campaigns() if data: campaigns = data.get_or_create_sub("campaigns") for campaign in campaigns.get_all("campaign"): name = campaign.get_text_val("name", "?") type = campaign.get_text_val("type", "") version = campaign.get_text_val("version", "") uploads = campaign.get_text_val("uploads", "") dependencies = campaign.get_text_val("dependencies", "") if re.escape(options.download).replace("\\_", "_") == options.download: if name == options.download: fetchlist.append((name, version, uploads, dependencies)) elif not options.type or options.type == type: if re.search(options.download, name): fetchlist.append((name, version, uploads, dependencies)) for name, version, uploads, dependencies in fetchlist: info = os.path.join(options.campaigns_dir, name, "_info.cfg") local_uploads, local_version = get_info(info) if uploads != local_uploads: # The uploads > local_uploads likely means a server reset if version != local_version or uploads > local_uploads: get(name, version, uploads, dependencies, options.campaigns_dir) else: print "Not downloading", name, \ "as the version already is", local_version, \ "(The add-on got re-uploaded.)" else: if options.verbose: print "Not downloading", name, \ "because it is already up-to-date." elif options.unpack: cs = CampaignClient(address) data = file(options.unpack).read() decoded = cs.decode(data) print "Unpacking %s..." % options.unpack cs.unpackdir(decoded, options.campaigns_dir, verbose=True) elif options.remove: cs = CampaignClient(address) data = cs.delete_campaign(options.remove, options.password) for message in data.find_all("message", "error"): print message.get_text_val("message") elif options.change_passphrase: cs = CampaignClient(address) data = cs.change_passphrase(*options.change_passphrase) for message in data.find_all("message", "error"): print message.get_text_val("message") elif options.upload: cs = CampaignClient(address) if os.path.isdir(options.upload): # else basename returns an empty string options.upload = options.upload.rstrip("/") # New style with _server.pbl pblfile = os.path.join(options.upload, "_server.pbl") name = os.path.basename(options.upload) wmldir = options.upload cfgfile = None # _main.cfg will be uploaded with the rest ignfile = os.path.join(options.upload, "_server.ign") else: # Old style with external .pbl file pblfile = options.upload name = os.path.basename(options.upload) name = os.path.splitext(name)[0] wmldir = os.path.join(os.path.dirname(options.upload), name) cfgfile = options.upload.replace(".pbl", ".cfg") ignfile = options.upload.replace(".pbl", ".ign") if options.pbl: pblfile = options.pbl pbl = wmldata.read_file(pblfile, "PBL") if os.path.exists(ignfile): ign = open(ignfile).readlines() # strip line endings and whitespace ign = [i.strip() for i in ign if i.strip()] else: ign = [ ".*", ".*/", "#*#", "*~", "*-bak", "*.swp", "*.pbl", "*.ign", "_info.cfg", "*.exe", "*.bat", "*.cmd", "*.com", "*.scr", "*.sh", "*.js", "*.vbs", "*.o", "Thumbs.db", "*.wesnoth", "*.project"] stuff = {} for field in ["title", "author", "passphrase", "description", "version", "icon", "type", "email", "translate"]: stuff[field] = pbl.get_text_val(field) mythread = cs.put_campaign_async(name, cfgfile, wmldir, ign, stuff) pcounter = 0 while not mythread.event.isSet(): mythread.event.wait(1) if cs.counter != pcounter: print "%d/%d" % (cs.counter, cs.length) pcounter = cs.counter for message in mythread.data.find_all("message", "error"): print message.get_text_val("message") elif options.update or options.status: if options.status: cdir = options.status else: cdir = options.update dirs = glob.glob(os.path.join(cdir, "*")) dirs = [x for x in dirs if os.path.isdir(x)] cs = CampaignClient(address) campaign_list = data = cs.list_campaigns() if not data: sys.stderr.write("Could not connect to the add-on server.\n") sys.exit(-1) campaigns = {} for c in data.get_or_create_sub("campaigns").get_all("campaign"): name = c.get_text_val("name") campaigns[name] = c for d in dirs: dirname = os.path.basename(d) if dirname in campaigns: info = os.path.join(d, "_info.cfg") sversion = campaigns[dirname].get_text_val("version", "") srev = campaigns[dirname].get_text_val("uploads", "") sdeps = campaigns[dirname].get_text_val("dependencies", "") if os.path.exists(info): lrev, lversion = get_info(info) if not srev: sys.stdout.write(" ? " + dirname + " - has no " + "version info on the server.\n") elif srev == lrev: sys.stdout.write(" " + dirname + " - is up to date.\n") elif sversion == lversion: sys.stdout.write(" # " + dirname + " - is version " + sversion + (" but you have revision %s not %s." + " (The add-on got re-uploaded.)\n") % (lrev, srev)) if srev > lrev: # server reset? if options.update: get(dirname, sversion, srev, sdeps, cdir) else: sys.stdout.write(" * " + dirname + " - you have " + "revision " + lrev + " but revision " + srev + " is available.\n") if options.update: get(dirname, sversion, srev, sdeps, cdir) else: sys.stdout.write(" ? " + dirname + " - is installed but has no " + "version info.\n") if options.update: get(dirname, sversion, srev, sdeps, cdir) else: sys.stdout.write(" - %s - is installed but not on server.\n" % dirname) elif options.html: pass else: optionparser.print_help() if options.html: if not campaign_list: cs = CampaignClient(address) campaign_list = cs.list_campaigns() del cs if campaign_list: import addon_manager.html addon_manager.html.output(options.html, options.url, campaign_list) else: sys.stderr.write("Could not retrieve campaign list " + "for HTML output.\n")