#! /usr/bin/env ruby $VERBOSE = nil # # add local lib dir to load path - http-access2 is included here for # convenience. # $:.unshift(File::join(File::dirname(File::dirname(__FILE__)), "lib")) # # load gems/libs # %w( getoptlong enumerator http-access2 yaml fileutils ).each do |l| begin require "rubygems"; require_gem l; rescue LoadError; require l end end # # hack to fix http-access2 cookie selection bug # module WebAgent::CookieUtils def domain_match(host, domain) case domain when /\d+\.\d+\.\d+\.\d+/ return (host == domain) when '.' return true when /^\./ #return tail_match?(domain, host) return tail_match?(host, domain) else return (host == domain) end end end # # defaults # PROGRAM = File::basename $0 HOME = ENV["HOME"] || ENV["HOMEPATH"] || File::expand_path("~") RUBYFORGE_D = File::join HOME, ".rubyforge" CONFIG_F = File::join RUBYFORGE_D, "config.yml" COOKIE_F = File::join RUBYFORGE_D, "cookie.dat" # # usage # USAGE = <<-txt SYNOPSIS #{ PROGRAM } [options]* mode [mode_args]* DESCRIPTION simplistic script which automates a limited set of rubyforge operations MODES setup() initializes your .rubyforge directory. you need to run this first before doing anything else. example : #{ PROGRAM } setup login() sends username and password from config.yml (or --username/--password options) and stores login cookie in cookie.dat. this is required for subsquent operations work. example : #{ PROGRAM } login #{ PROGRAM } login --username zaphod --password 42 create_package(group_id, package_name) creates the named package under the specified group. example : #{ PROGRAM } create_package 1024 traits #{ PROGRAM } login && #{ PROGRAM } create_package codeforpeople.com traits notes : in order to use group_ids by name, rather than number, you must edit the rubyforge[group_ids] translation table in your config.yml. add_release(group_id, package_id, release_name, userfile) release a file as release_name under the specified group_id and package_id. example : #{ PROGRAM } add_release codeforpeople.com traits 0.8.0 traits-0.8.0.gem #{ PROGRAM } add_release codeforpeople.com traits 0.8.0 traits-0.8.0.tgz #{ PROGRAM } add_release 1024 1242 0.8.0 traits-0.8.0.gem #{ PROGRAM } login && #{ PROGRAM } add_release 1024 1242 0.8.0 traits-0.8.0.gem notes : in order to use group_ids and package_ids by name, rather than number, you must edit the rubyforge[group_ids] and rubyforge[package_ids] translation tables in your config.yml. delete_package(group_id, package_name) deletes a package and all it's files. example : #{ PROGRAM } delete_package codeforpeople.com traits #{ PROGRAM } delete_package 1024 traits NOTES - you can determine the group_id and package_id of projects and packages by login -> my page tab -> select a project link from 'my projects' -> files tab -> admin link (not the admin tab!) -> now you'll be at page listing your packages in this project. near the bottom you'll see links to 'add a release' or 'edit a release' - hover over the url and you'll notice the query string, which looks something like ?package_id=1242&group_id=1024 and that's what you need to know - don't forget to login! logging in will store a cookie in your .rubyforge directory which expires after a time. always run the login command before any operation that requires authentication, such as uploading a package. TODO - scrape rubyforge to auto-configure group_id and package_ids. - objectify the script. it's procedural butchery attm. - add error checking. this requires screen scraping to see of an operation succeeded since 200 is returned from rubyforge even for failed operations and only the html text reveals the status. - add more functionality. OPTIONS global : --help , -h this message --config , -c specify a config file (default #{ CONFIG_F }) --username , -u specify username, taken from config otherwise --password , -p specify password, taken from config otherwise --cookie_jar , -C specify cookie storage file (default #{ COOKIE_F }) add_release : --is_private , -P if true, release is not public --release_date , -r specify time of release (default 'now') --type_id , -t specify filetype code (default determined by ext) --processor_id , -o specify processor (default 'Any') --release_notes , -n specify release notes as string or file --release_changes , -a specify release changes as string or file --preformatted , -f specify whether release_notes/changes are preformatted txt USAGE.gsub! %r|^#{ USAGE[%r/^\s*/] }|, '' # # config # CONFIG = <<-yml # # base rubyforge uri - store in #{ CONFIG_F } # uri : http://rubyforge.org # # this must be your username # username : username # # this must be your password # password : password # # defaults for some values # defaults : cookie_jar : #{ COOKIE_F } is_private : false # # server side rubyforge configuration # rubyforge : # # map your package names to their rubyforge ids # package_ids : traits : 1241 # # map your group names to their rubyforge ids # group_ids : codeforpeople.com : 1024 # # mapping file exts to rubyforge ids # type_ids : .deb : 1000 .rpm : 2000 .zip : 3000 .bz2 : 3100 .gz : 3110 .src.zip : 5000 .src.bz2 : 5010 .src.tar.bz2 : 5010 .src.gz : 5020 .src.tar.gz : 5020 .src.rpm : 5100 .src : 5900 .jpg : 8000 .txt : 8100 .text : 8100 .htm : 8200 .html : 8200 .pdf : 8300 .oth : 9999 .ebuild : 1300 .exe : 1100 .dmg : 1200 .tar.gz : 5000 .tgz : 5000 .gem : 1400 .pgp : 8150 .sig : 8150 # # map processor names to rubyforge ids # processor_ids : i386 : 1000 IA64 : 6000 Alpha : 7000 Any : 8000 PPC : 2000 MIPS : 3000 Sparc : 4000 UltraSparc : 5000 Other : 9999 yml CONFIG.gsub! %r|^#{ CONFIG[%r/^\s*/] }|, '' # # load mode, global opts, and config # mode = ARGV.shift opts = GetoptLong::new( [ "--help" , "-h" , GetoptLong::REQUIRED_ARGUMENT ] , [ "--config" , "-c" , GetoptLong::REQUIRED_ARGUMENT ] , [ "--username" , "-u" , GetoptLong::REQUIRED_ARGUMENT ] , [ "--password" , "-p" , GetoptLong::REQUIRED_ARGUMENT ] , [ "--cookie_jar" , "-C" , GetoptLong::REQUIRED_ARGUMENT ] , [ "--is_private", "-P", GetoptLong::REQUIRED_ARGUMENT ], [ "--release_date" , "-r" , GetoptLong::REQUIRED_ARGUMENT ] , [ "--type_id" , "-t" , GetoptLong::REQUIRED_ARGUMENT ] , [ "--processor_id" , "-o" , GetoptLong::REQUIRED_ARGUMENT ] , [ "--release_notes" , "-n" , GetoptLong::REQUIRED_ARGUMENT ] , [ "--release_changes" , "-a" , GetoptLong::REQUIRED_ARGUMENT ] , [ "--preformatted" , "-f" , GetoptLong::NO_ARGUMENT ] ).enum_for.inject({}){|h,kv| h.update kv.first.delete('-') => kv.last} config = opts["config"] || CONFIG_F config = test(?e, config) ? IO::read(config) : CONFIG config = YAML::load config username = opts["username"] || config["username"] password = opts["password"] || config["password"] cookie_jar = opts["cookie_jar"] || config["defaults"]["cookie_jar"] abort "no " unless username abort "no " unless password abort "no " unless cookie_jar mode = "help" if opts["help"] # # run based on mode # msg, page, form, extheader = nil, nil, nil, {} case mode # # help mode # when %r/help/ USAGE.display exit # # setup mode # when %r/setup/ FileUtils::mkdir_p RUBYFORGE_D test ?e, CONFIG_F and FileUtils::mv CONFIG_F, "#{ CONFIG_F }.bak" open(CONFIG_F,"w"){|f| f.write CONFIG} FileUtils::touch COOKIE_F edit = (ENV["EDITOR"] || ENV["EDIT"] || "gvim") + " #{ CONFIG_F }" system edit or puts "edit #{ CONFIG_F }" exit # # login mode # when %r/login/ page, msg = "/account/login.php", "post_content" form = { "return_to" => "", "form_loginname" => username, "form_pw" => password, "login" => "Login" } # # create_package mode # when %r/create_package/ page, msg = "/frs/admin/index.php", "post_content" group_id, package_name = ARGV abort "no " unless group_id abort "no " unless package_name unless group_id.to_s =~ %r/^\d+$/ key = group_id.to_s group_id = config["rubyforge"]["group_ids"][key] abort "no configured for <#{ key }>" unless group_id end is_private = opts["is_private"] || config["defaults"]["is_private"] is_public = is_private ? 0 : 1 form = { "func" => "add_package", "group_id" => group_id, "package_name" => package_name, "is_public" => is_public, "submit" => "Create This Package", } # # delete_package mode # when %r/delete_package/ page, msg = "/frs/admin/index.php", "post_content" group_id, package_id = ARGV abort "no " unless group_id abort "no " unless package_id unless group_id.to_s =~ %r/^\d+$/ key = group_id.to_s group_id = config["rubyforge"]["group_ids"][key] abort "no configured for <#{ key }>" unless group_id end unless package_id.to_s =~ %r/^\d+$/ key = package_id.to_s package_id = config["rubyforge"]["package_ids"][key] abort "no configured for <#{ key }>" unless package_id end form = { "func" => "delete_package", "group_id" => group_id, "package_id" => package_id, "sure" => "1", "really_sure" => "1", "submit" => "Delete", } # # add_release mode # when %r/add_release/ page, msg = "/frs/admin/qrs.php", "post_content" group_id, package_id, release_name, userfile = ARGV abort "no " unless group_id abort "no " unless package_id abort "no " unless release_name abort "no " unless userfile unless group_id.to_s =~ %r/^\d+$/ key = group_id.to_s group_id = config["rubyforge"]["group_ids"][key] abort "no configured for <#{ key }>" unless group_id end unless package_id.to_s =~ %r/^\d+$/ key = package_id.to_s package_id = config["rubyforge"]["package_ids"][key] abort "no configured for <#{ key }>" unless package_id end userfile = open userfile release_date = opts["release_date"] type_id = opts["type_id"] processor_id = opts["processor_id"] release_notes = opts["release_notes"] release_changes = opts["release_changes"] preformatted = opts["preformatted"] release_date ||= Time::now.strftime("%Y-%m-%d %H:%M") type_id ||= userfile.path[%r|\.[^\./]+$|] unless type_id.to_s =~ %r/^\d+$/ key = type_id.to_s type_id = config["rubyforge"]["type_ids"][key] abort "no configured for <#{ key }>" unless type_id end processor_id ||= "Any" unless processor_id.to_s =~ %r/^\d+$/ key = processor_id.to_s processor_id = config["rubyforge"]["processor_ids"][key] abort "no configured for <#{ key }>" unless processor_id end release_notes = IO::read(release_notes) if release_notes and test(?e, release_notes) release_changes = IO::read(release_changes) if release_changes and test(?e, release_changes) preformatted = preformatted ? 1 : 0 form = { "group_id" => group_id, "package_id" => package_id, "release_name" => release_name, "release_date" => release_date, "type_id" => type_id, "processor_id" => processor_id, "release_notes" => release_notes, "release_changes" => release_changes, "preformatted" => preformatted, "userfile" => userfile, "submit" => "Release File" } boundary = Array::new(8){ "%2.2d" % rand(42) }.join('__') extheader['content-type'] = "multipart/form-data; boundary=___#{ boundary }___" # # bad mode # else abort USAGE end # # commit http transaction # if [msg, page, form].all? client = HTTPAccess2::Client::new ENV["HTTP_PROXY"] client.debug_dev = STDERR if ENV["RUBYFORGE_DEBUG"] || ENV["DEBUG"] client.set_cookie_store cookie_jar # # hack to fix http-access2 redirect bug/feature # client.redirect_uri_callback = lambda do |res| page = res.header['location'].first page =~ %r/http/ ? page : "#{ config['uri'] }/#{ page }" end response = client.send "#{ msg }", "#{ config['uri'] }/#{ page }", form, extheader client.save_cookie_store end exit 0