rhype 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY ADDED
File without changes
@@ -0,0 +1,25 @@
1
+ bin
2
+ lib
3
+ meta
4
+ README
5
+ HISTORY
6
+ VERSION
7
+ NEWS
8
+ COPYING
9
+ bin/rhype
10
+ lib/rhype
11
+ lib/rhype/command.rb
12
+ lib/rhype/host.rb
13
+ lib/rhype/command.help
14
+ lib/rhype/controller.rb
15
+ lib/rhype/service.rb
16
+ lib/rhype/metadata.rb
17
+ lib/rhype/index.rb
18
+ lib/rhype/hosts
19
+ lib/rhype/hosts/rubyforge.rb
20
+ lib/rhype/hosts/gforge.rb
21
+ meta/homepage
22
+ meta/unixname
23
+ meta/author
24
+ meta/description
25
+ meta/contact
data/NEWS ADDED
@@ -0,0 +1,22 @@
1
+ I didn't really wnat to create this project. Honest.
2
+ But there were a couple of things I didn't like about
3
+ the rubyforge gem: it dummped config info into ones
4
+ home directory (without using the XDG standard) and
5
+ it couldn't automatically login.
6
+
7
+ Besides that I figure I could probably make the tool
8
+ a little more flexible and support additional hosts
9
+ as needed.
10
+
11
+ So that's how RHype was born. Originally this code
12
+ has been dwelling as a support library in the Reap
13
+ project. Hey, now it's free to live it's own life :)
14
+
15
+ Acknowlegement go to Ara T. Howard who wrote the
16
+ original rubyfoorge.rb script on which this code is
17
+ largely based.
18
+
19
+ ### 0.1.0 / 2008-11-24
20
+
21
+ * Initial Release.
22
+
data/README ADDED
@@ -0,0 +1,13 @@
1
+ = RHype
2
+
3
+ RHype is a commandline interface for interacting with online project hosts.
4
+
5
+ == Install
6
+
7
+ gem install rhype
8
+
9
+ == Copyright
10
+
11
+ RHype (c) 2008 TigerOps
12
+ Distributed under the terms of the GPLv3.
13
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ rhype 0.1.0 beta (2008-11-23)
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rhype/command'
4
+
5
+ RHype::Command.start
6
+
@@ -0,0 +1,29 @@
1
+ Usage: hype <action>[:host] [options]
2
+
3
+ Actions can very from host to host, but hosts generally
4
+ offer one or more of the following services:
5
+
6
+ publish Publish website
7
+ release Release package
8
+ announce Announce release
9
+ touch Check host availability
10
+
11
+ Specifying a host name limits the action to that specific host.
12
+ If no host name is given, then each host that responds to the
13
+ given action will be invoked in turn. A confirmation prompt will
14
+ appear for each host unless --force is used.
15
+
16
+ Action and host options can be given on the command line using
17
+ opt=arg notation. To facilitate easy reuse, however, place options
18
+ in the hype configuration file.
19
+
20
+ The following options are for the commandline only and are common
21
+ to all commands.
22
+
23
+ -n --dryrun Only pretend to do action
24
+ -d --debug Output detailed error reports
25
+ -f --force Automatically answer 'y' to queires
26
+ -h --help Display help information
27
+
28
+ See API documentation to learn the options for each action and host.
29
+
@@ -0,0 +1,82 @@
1
+ require 'facets/argvector'
2
+ require 'rhype/controller'
3
+
4
+ module RHype
5
+
6
+ # = Command
7
+ #
8
+ # Provides CLI interface to RHype Controller.
9
+ class Command
10
+
11
+ HELPFILE = File.join(File.dirname(__FILE__), 'command.help')
12
+
13
+ attr :hostname
14
+
15
+ attr :action
16
+
17
+ attr :arguments
18
+
19
+ attr :options
20
+
21
+ #
22
+ def initialize
23
+ argv = Argvector.new
24
+
25
+ args, opts = *argv.parameters
26
+ cmd = args.shift
27
+
28
+ opts.each do |key, val|
29
+ case key
30
+ when 'help', 'h'
31
+ puts File.read(HELPFILE)
32
+ exit 0
33
+ when 'dryrun'
34
+ $DRYRUN = true
35
+ when 'quiet'
36
+ $QUIET = true
37
+ when 'force'
38
+ $FORCE = true
39
+ when 'trace'
40
+ $TRACE = true
41
+ when 'debug'
42
+ $DEBUG = true
43
+ end
44
+ end
45
+
46
+ unless cmd
47
+ puts "No command given."
48
+ exit -1
49
+ end
50
+
51
+ action, hostname = *cmd.split(':')
52
+
53
+ @hostname = hostname
54
+ @action = action
55
+ @arguments = args
56
+ @options = opts
57
+ end
58
+
59
+ #
60
+ def call
61
+ begin
62
+ app = Controller.new(hostname, options)
63
+ app.call(action) #arguments)
64
+ rescue => e
65
+ if $TRACE || $DEBUG
66
+ raise e
67
+ else
68
+ puts e.to_s
69
+ end
70
+ end
71
+ end
72
+
73
+ # class methods ------------------------------------------------
74
+
75
+ def self.start
76
+ new.call
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+
@@ -0,0 +1,71 @@
1
+ require 'rhype/hosts/gforge'
2
+ require 'rhype/hosts/rubyforge'
3
+ #require 'rhype/hosts/email'
4
+
5
+ module RHype
6
+
7
+ class Controller
8
+
9
+ CONFIG_FILE = '.config/rhype/config.yaml'
10
+
11
+ attr :hosts
12
+
13
+ attr :config
14
+
15
+ attr :options
16
+
17
+ # New RHype Controller.
18
+ def initialize(hostname, options)
19
+ @options = options.rekey(&:to_s)
20
+
21
+ load_configuration
22
+
23
+ if hostname
24
+ @hosts = [hostname.to_s.downcase]
25
+ else
26
+ @hosts = config.keys
27
+ end
28
+
29
+ @host_cache = {}
30
+ end
31
+
32
+ # Load configuration.
33
+ def load_configuration
34
+ file = Dir.glob(CONFIG_FILE, File::FNM_CASEFOLD).first
35
+ if file
36
+ @config = YAML::load(File.new(file))
37
+ else
38
+ #raise "#{CONFIG_FILE} not found"
39
+ @config = {}
40
+ RHype.register.each do |key, klass|
41
+ @config[key] = {'type' => key}
42
+ end
43
+ end
44
+ end
45
+
46
+ # Invoke action.
47
+ def call(action) #, arguments)
48
+ raise "No hosts." if hosts.empty?
49
+ # loop thru hosts and invoke action.
50
+ hosts.each do |name|
51
+ opts = config[name] || {}
52
+ type = opts.delete('type')
53
+ opts = opts.merge(options)
54
+ host(name, type, opts).__send__(action, options)
55
+ end
56
+ end
57
+
58
+ #
59
+ def host(name, type, options)
60
+ name = name.to_sym
61
+ @host_cache[name] ||= (
62
+ klass = Host[type]
63
+ raise "Undefined host name: #{name}" unless klass
64
+ klass.new(options)
65
+ )
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+
@@ -0,0 +1,55 @@
1
+ require 'rhype/service'
2
+
3
+ module RHype
4
+
5
+ def self.register
6
+ @register ||= {}
7
+ end
8
+
9
+ # = Host
10
+ #
11
+ # Base class for Host Services.
12
+ #
13
+ class Host < Service
14
+
15
+ def self.[](name)
16
+ RHype.register[name.to_s.downcase]
17
+ end
18
+
19
+ def self.inherited(base)
20
+ name = base.to_s.split('::').last.downcase
21
+ RHype.register[name] = base
22
+ end
23
+
24
+ # Generic announce confirmation.
25
+ # "Release to #{self.class.basename.downcase}?"
26
+ def confirm?(message, options={})
27
+ return true if force?
28
+ ans = ask(message, "yN")
29
+ case ans.downcase
30
+ when 'y', 'yes'
31
+ true
32
+ else
33
+ false
34
+ end
35
+ end
36
+
37
+ DEFAULT_ANNOUNCEMENT = "doc/ann{,ounce}{.txt,.rdoc}"
38
+
39
+ #ans = ask("Announce to #{self.class.basename.downcase}?", "yN")
40
+
41
+ #
42
+
43
+ def announcement(file=nil)
44
+ template = file || DEFAULT_ANNOUNCEMENT
45
+ project.generate('templates'=>template)
46
+ file = Dir.glob(template, File::FNM_CASEFOLD).first
47
+ text = File.read(file)
48
+ text = unfold_paragraphs(text)
49
+ text
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+
@@ -0,0 +1,755 @@
1
+ # TODO: remove these two dependencies
2
+ #require 'facets' #/hash/rekey'
3
+ require 'facets/kernel/ask'
4
+
5
+ require 'ostruct'
6
+ require 'httpclient'
7
+
8
+ require 'rhype/host'
9
+
10
+ module RHype
11
+
12
+ # = GForge
13
+ #
14
+ # Interface with the GForge based hosting services.
15
+ # Supports the following tasks:
16
+ #
17
+ # * release - Upload release packages
18
+ # * publish - Publish website
19
+ # * announce - Post news announcement
20
+ # * touch - Test connection
21
+ #
22
+ class GForge < Host
23
+
24
+ #HOME = ENV["HOME"] || ENV["HOMEPATH"] || File.expand_path("~")
25
+ COOKIEJAR = File::join(Dir.tmpdir, 'reap', 'cookie.dat')
26
+ REPORT = /<h\d><span style="color:red">(.*?)<\/span><\/h\d>/
27
+
28
+ # Project unixname.
29
+ attr_accessor :unixname
30
+
31
+ # Project name.
32
+ attr_accessor :version
33
+
34
+ # Project's group id number.
35
+ attr_accessor :group_id
36
+
37
+ alias_method :group, :group_id
38
+
39
+ # Username for project account.
40
+ attr_accessor :username
41
+
42
+ # Password for project account.
43
+ attr_accessor :password
44
+
45
+ #
46
+ attr_accessor :domain
47
+
48
+ private
49
+
50
+ def initialize(options) #, spec, mode)
51
+ #@unixname = unixname
52
+
53
+ options.each do |k,v|
54
+ send("#{k}=", v) if respond_to?("#{k}=")
55
+ end
56
+
57
+ raise "missing unixname in #{self.class.name}" unless @unixname
58
+ raise "missing domain in #{self.class.name}" unless @domain
59
+
60
+ @package_ids = {}
61
+ @release_ids = {}
62
+ @file_ids = {}
63
+
64
+ FileUtils.mkdir_p(File.dirname(COOKIEJAR))
65
+ end
66
+
67
+ #def initialize_defaults
68
+ # @unixname = metadata.unixname
69
+ # @version = metadata.version
70
+ #end
71
+
72
+ public
73
+
74
+ # URI = http:// + domain name
75
+ #
76
+ # TODO: Deal with https, and possible other protocols too.
77
+
78
+ def uri
79
+ @uri ||= URI.parse("http://" + domain)
80
+ end
81
+
82
+ #
83
+
84
+ def cookie_jar
85
+ COOKIEJAR
86
+ end
87
+
88
+ public
89
+
90
+ # Website location on server.
91
+ def siteroot
92
+ "/var/www/gforge-projects"
93
+ end
94
+
95
+ # What commands does this host support.
96
+
97
+ def commands
98
+ %w{ touch release publish post }
99
+ end
100
+
101
+
102
+ # Login to website.
103
+
104
+ def login # :yield:
105
+ load_project_cached
106
+
107
+ page = @uri + "/account/login.php"
108
+ page.scheme = 'https'
109
+ page = URI.parse(page.to_s) # set SSL port correctly
110
+
111
+ form = {
112
+ "return_to" => "",
113
+ "form_loginname" => username,
114
+ "form_pw" => password,
115
+ "login" => "Login with SSL"
116
+ }
117
+ html = http_post(page, form)
118
+
119
+ if not html[/Personal Page/]
120
+ puts "Login failed."
121
+ re1 = Regexp.escape(%{<h2 style="color:red">})
122
+ re2 = Regexp.escape(%{</h2>})
123
+ html[/#{re1}(.*?)#{re2}/]
124
+ raise $1
125
+ else
126
+ @printed_project_name ||= (puts "Project: #{unixname}"; true)
127
+ end
128
+
129
+ if block_given?
130
+ begin
131
+ yield
132
+ ensure
133
+ logout
134
+ end
135
+ end
136
+ end
137
+
138
+ # Logout of website.
139
+
140
+ def logout
141
+ page = "/account/logout.php"
142
+ form = {}
143
+ http_post(page, form)
144
+ end
145
+
146
+ # Touch base with server -- login and logout.
147
+
148
+ def touch(options={})
149
+ login
150
+ puts "Group ID: #{group_id}"
151
+ puts "Login/Logout successful."
152
+ logout
153
+ end
154
+
155
+ # Upload release packages to hosting service.
156
+ #
157
+ # This task releases files to RubyForge --it should work with other
158
+ # GForge instaces or SourceForge clones too.
159
+ #
160
+ # While defaults are nice, you may want a little more control. You can
161
+ # specify additional attributes:
162
+ #
163
+ # files package files to release.
164
+ # exclude Package formats to exclude from files.
165
+ # (from those created by pack)
166
+ # unixname Project name on host.
167
+ # package Package to which this release belongs (defaults to project)
168
+ # release Release name (default is version number)
169
+ # version Version of release
170
+ # date Date of release (defaults to Time.now)
171
+ # processor Processor/Architecture (any, i386, PPC, etc.)
172
+ # is_public Public release? (defualts to true)
173
+ # changelog Change log file
174
+ # notelog Release notes file
175
+ #
176
+ # The release option can be a template by using %s in the
177
+ # string. The version number of your project will be sub'd
178
+ # in for the %s. This saves you from having to update
179
+ # the release name before every release.
180
+ #
181
+ #--
182
+ # What about releasing a pacman PKGBUILD?
183
+ #++
184
+
185
+ def release(options)
186
+ options = options.rekey
187
+
188
+ version = options[:version] || metadata.version
189
+ changelog = options[:changelog]
190
+ notelog = options[:notelog]
191
+
192
+ unixname = options[:unixname] || unixname()
193
+ package = options[:package] || unixname()
194
+ release = options[:release] || version()
195
+ name = options[:name] || package
196
+ files = options[:file] || []
197
+ date = options[:date] || Time::now.strftime('%Y-%m-%d %H:%M')
198
+ processor = options[:processor] || 'Any'
199
+ store = options[:store] || 'pkg'
200
+
201
+ is_public = options[:is_public].nil? ? true : options[:is_public]
202
+
203
+ raise ArgumentError, "missing unixname" unless unixname
204
+ raise ArgumentError, "missing package" unless package
205
+ raise ArgumentError, "missing release" unless release
206
+
207
+ if files.empty?
208
+ files = Dir[File.join(store, '*')].select do |file|
209
+ /#{version}[.]/ =~ file
210
+ end
211
+ #files = Dir.glob(File.join(store,"#{name}-#{version}*"))
212
+ end
213
+
214
+ files = files.select{ |f| File.file?(f) }
215
+
216
+ abort "No package files." if files.empty?
217
+
218
+ files.each do |file|
219
+ abort "Not a file -- #{file}" unless File.exist?(file)
220
+ puts "Release file: #{file}"
221
+ end
222
+
223
+ # which package types
224
+ #rtypes = [ 'tgz', 'tbz', 'tar.gz', 'tar.bz2', 'deb', 'gem', 'ebuild', 'zip' ]
225
+ #rtypes -= exclude
226
+ #rtypes = rtypes.collect{ |rt| Regexp.escape( rt ) }
227
+ #re_rtypes = Regexp.new('[.](' << rtypes.join('|') << ')$')
228
+
229
+ puts "Releasing #{package} #{release}..." #unless options['quiet']
230
+
231
+ login do
232
+
233
+ raise ArgumentError, "missing group_id" unless group_id
234
+
235
+ unless package_id = package?(package)
236
+ if dryrun?
237
+ puts "Package '#{package}' does not exist."
238
+ puts "Create package #{package}."
239
+ abort "Cannot continue in dryrun mode."
240
+ else
241
+ #unless options['force']
242
+ q = "Package '#{package}' does not exist. Create?"
243
+ a = ask(q, 'yN')
244
+ abort "Task canceled." unless ['y', 'yes', 'okay'].include?(a.downcase)
245
+ #end
246
+ puts "Creating package #{package}..."
247
+ create_package(package, is_public)
248
+ unless package_id = package?(package)
249
+ raise "Package creation failed."
250
+ end
251
+ end
252
+ end
253
+ if release_id = release?(release, package_id)
254
+ #unless options[:force]
255
+ if dryrun?
256
+ puts "Release #{release} already exists."
257
+ else
258
+ q = "Release #{release} already exists. Re-release?"
259
+ a = ask(q, 'yN')
260
+ abort "Task canceled." unless ['y', 'yes', 'okay'].include?(a.downcase)
261
+ #puts "Use -f option to force re-release."
262
+ #return
263
+ end
264
+ files.each do |file|
265
+ fname = File.basename(file)
266
+ if file_id = file?(fname, package)
267
+ if dryrun?
268
+ puts "Remove file #{fname}."
269
+ else
270
+ puts "Removing file #{fname}..."
271
+ remove_file(file_id, release_id, package_id)
272
+ end
273
+ end
274
+ if dryrun?
275
+ puts "Add file #{fname}."
276
+ else
277
+ puts "Adding file #{fname}..."
278
+ add_file(file, release_id, package_id, processor)
279
+ end
280
+ end
281
+ else
282
+ if dryrun?
283
+ puts "Add release #{release}."
284
+ else
285
+ puts "Adding release #{release}..."
286
+ add_release(release, package_id, files,
287
+ :processor => processor,
288
+ :release_date => date,
289
+ :release_changes => changelog,
290
+ :release_notes => notelog,
291
+ :preformatted => '1'
292
+ )
293
+ unless release_id = release?(release, package_id)
294
+ raise "Release creation failed."
295
+ end
296
+ end
297
+ #files.each do |file|
298
+ # puts "Added file #{File.basename(file)}."
299
+ #end
300
+ end
301
+ end
302
+ puts "Release complete!" unless dryrun?
303
+ end
304
+
305
+ # #
306
+ # # Publish documents to website.
307
+ # #
308
+ # # TODO Fix publish method for Rubyforge tool.
309
+ #
310
+ # def publish(options)
311
+ # options = options.rekey
312
+ #
313
+ # #domain = options[:domain] || DOMAIN
314
+ # root = File.join(siteroot, unixname)
315
+ # root = File.join(root, options[:root]) if options[:root]
316
+ #
317
+ # options.update(
318
+ # :host => domain,
319
+ # :root => root
320
+ # )
321
+ #
322
+ # UploadUtils.rsync(options)
323
+ # end
324
+
325
+ # Submit a news item.
326
+
327
+ def announce(options)
328
+ options = options.rekey
329
+
330
+ if file = options[:file]
331
+ text = File.read(file).strip
332
+ i = text.index("\n")
333
+ subject = text[0...i].strip
334
+ message = text[i..-1].strip
335
+ else
336
+ subject = options[:subject]
337
+ message = options[:message] || options[:body]
338
+ end
339
+
340
+ if dryrun?
341
+ puts "announce-rubyforge: #{subject}"
342
+ else
343
+ post_news(subject, message)
344
+ puts "News item posted!"
345
+ end
346
+ end
347
+
348
+
349
+ private
350
+
351
+ # HTTP POST transaction.
352
+
353
+ def http_post(page, form, extheader={})
354
+ client = HTTPClient::new ENV["HTTP_PROXY"]
355
+ client.debug_dev = STDERR if ENV["REAP_DEBUG"] || $DEBUG
356
+ client.set_cookie_store(cookie_jar)
357
+ client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
358
+
359
+ # HACK to fix http-client redirect bug/feature
360
+ client.redirect_uri_callback = lambda do |uri, res|
361
+ page = res.header['location'].first
362
+ page =~ %r/http/ ? page : @uri + page
363
+ end
364
+
365
+ uri = @uri + page
366
+ if $DEBUG then
367
+ puts "POST #{uri.inspect}"
368
+ puts "#{form.inspect}"
369
+ puts "#{extheader.inspect}" unless extheader.empty?
370
+ puts
371
+ end
372
+
373
+ response = client.post_content uri, form, extheader
374
+
375
+ if response[REPORT]
376
+ puts "(" + $1 + ")"
377
+ end
378
+
379
+ client.save_cookie_store
380
+
381
+ return response
382
+ end
383
+
384
+ #
385
+
386
+ def load_project_cached
387
+ @load_project_cache ||= load_project
388
+ end
389
+
390
+ # Loads information for project: group_id, package_ids and release_ids.
391
+
392
+ def load_project
393
+ html = URI.parse("http://#{domain}/projects/#{unixname}/index.html").read
394
+
395
+ group_id = html[/(frs|tracker)\/\?group_id=\d+/][/\d+/].to_i
396
+ @group_id = group_id
397
+
398
+ if $DEBUG
399
+ puts "GROUP_ID = #{group_id}"
400
+ end
401
+
402
+ html = URI.parse("http://rubyforge.org/frs/?group_id=#{group_id}").read
403
+
404
+ package = nil
405
+ html.scan(/<h3>[^<]+|release_id=\d+">[^>]+|filemodule_id=\d+/).each do |s|
406
+ case s
407
+ when /<h3>([^<]+)/ then
408
+ package = $1.strip
409
+ when /filemodule_id=(\d+)/ then
410
+ @package_ids[package] = $1.to_i
411
+ when /release_id=(\d+)">([^<]+)/ then
412
+ package_id = @package_ids[package]
413
+ @release_ids[[package_id,$2]] = $1.to_i
414
+ end
415
+ end
416
+
417
+ if $DEBUG
418
+ p @package_ids, @release_ids
419
+ end
420
+ end
421
+
422
+ # Returns password. If not already set, will ask for it.
423
+
424
+ def password
425
+ @password ||= ENV['RUBYFORGE_PASSWORD']
426
+ @password ||= (
427
+ print "Password for #{username}: "
428
+ until inp = $stdin.gets ; sleep 1 ; end ; puts
429
+ inp.strip
430
+ )
431
+ end
432
+
433
+ # Package exists? Returns package-id number.
434
+
435
+ def package?(package_name)
436
+ id = @package_ids[package_name]
437
+ return id if id
438
+
439
+ package_id = nil
440
+
441
+ page = "/frs/"
442
+
443
+ form = {
444
+ "group_id" => group_id
445
+ }
446
+ scrape = http_post(page, form)
447
+
448
+ restr = ''
449
+ restr << Regexp.escape( package_name )
450
+ restr << '\s*'
451
+ restr << Regexp.escape( '<a href="/frs/monitor.php?filemodule_id=' )
452
+ restr << '(\d+)'
453
+ restr << Regexp.escape( %{&group_id=#{group_id}} )
454
+ re = Regexp.new( restr )
455
+
456
+ md = re.match( scrape )
457
+ if md
458
+ package_id = md[1]
459
+ end
460
+
461
+ @package_ids[package_name] = package_id
462
+ end
463
+
464
+ # Create a new package.
465
+
466
+ def create_package( package_name, is_public=true )
467
+ page = "/frs/admin/index.php"
468
+
469
+ form = {
470
+ "func" => "add_package",
471
+ "group_id" => group_id,
472
+ "package_name" => package_name,
473
+ "is_public" => (is_public ? 1 : 0),
474
+ "submit" => "Create This Package"
475
+ }
476
+
477
+ http_post(page, form)
478
+ end
479
+
480
+ # Delete package.
481
+
482
+ def delete_package(package_id)
483
+ page = "/frs/admin/index.php"
484
+
485
+ form = {
486
+ "func" => "delete_package",
487
+ "group_id" => group_id,
488
+ "package_id" => package_id,
489
+ "sure" => "1",
490
+ "really_sure" => "1",
491
+ "submit" => "Delete",
492
+ }
493
+
494
+ http_post(page, form)
495
+ end
496
+
497
+ # Release exits? Returns release-id number.
498
+
499
+ def release?(release_name, package_id)
500
+ id = @release_ids[[release_name,package_id]]
501
+ return id if id
502
+
503
+ release_id = nil
504
+
505
+ page = "/frs/admin/showreleases.php"
506
+
507
+ form = {
508
+ "package_id" => package_id,
509
+ "group_id" => group_id
510
+ }
511
+ scrape = http_post( page, form )
512
+
513
+ restr = ''
514
+ restr << Regexp.escape( %{"editrelease.php?group_id=#{group_id}} )
515
+ restr << Regexp.escape( %{&amp;package_id=#{package_id}} )
516
+ restr << Regexp.escape( %{&amp;release_id=} )
517
+ restr << '(\d+)'
518
+ restr << Regexp.escape( %{">#{release_name}} )
519
+ re = Regexp.new( restr )
520
+
521
+ md = re.match( scrape )
522
+ if md
523
+ release_id = md[1]
524
+ end
525
+
526
+ @release_ids[[release_name,package_id]] = release_id
527
+ end
528
+
529
+ # Add a new release.
530
+
531
+ def add_release(release_name, package_id, *files)
532
+ page = "/frs/admin/qrs.php"
533
+
534
+ options = (Hash===files.last ? files.pop : {}).rekey
535
+ files = files.flatten
536
+
537
+ processor = options[:processor]
538
+ release_date = options[:release_date]
539
+ release_changes = options[:release_changes]
540
+ release_notes = options[:release_notes]
541
+
542
+ release_date ||= Time::now.strftime("%Y-%m-%d %H:%M")
543
+
544
+ file = files.shift
545
+ puts "Adding file #{File.basename(file)}..."
546
+ userfile = open(file, 'rb')
547
+
548
+ type_id = userfile.path[%r|\.[^\./]+$|]
549
+ type_id = FILETYPES[type_id]
550
+ processor_id = PROCESSORS[processor.downcase]
551
+
552
+ # TODO IS THIS WORKING?
553
+ release_notes = IO::read(release_notes) if release_notes and test(?f, release_notes)
554
+ release_changes = IO::read(release_changes) if release_changes and test(?f, release_changes)
555
+
556
+ preformatted = '1'
557
+
558
+ form = {
559
+ "group_id" => group_id,
560
+ "package_id" => package_id,
561
+ "release_name" => release_name,
562
+ "release_date" => release_date,
563
+ "type_id" => type_id,
564
+ "processor_id" => processor_id,
565
+ "release_notes" => release_notes,
566
+ "release_changes" => release_changes,
567
+ "preformatted" => preformatted,
568
+ "userfile" => userfile,
569
+ "submit" => "Release File"
570
+ }
571
+
572
+ boundary = Array::new(8){ "%2.2d" % rand(42) }.join('__')
573
+ boundary = "multipart/form-data; boundary=___#{ boundary }___"
574
+
575
+ html = http_post(page, form, 'content-type' => boundary)
576
+
577
+ release_id = html[/release_id=\d+/][/\d+/].to_i
578
+ puts "RELEASE ID = #{release_id}" if $DEBUG
579
+
580
+ files.each do |file|
581
+ puts "Adding file #{File.basename(file)}..."
582
+ add_file(file, release_id, package_id, processor)
583
+ end
584
+
585
+ release_id
586
+ end
587
+
588
+ # File exists?
589
+ #
590
+ # NOTE this is a bit fragile. If two releases have the same exact
591
+ # file name in them there could be a problem --that's probably not
592
+ # likely, but I can't yet rule it out.
593
+ #
594
+ # TODO Remove package argument, it is no longer needed.
595
+
596
+ def file?(file, package)
597
+ id = @file_ids[[file, package]]
598
+ return id if id
599
+
600
+ file_id = nil
601
+
602
+ page = "/frs/"
603
+
604
+ form = {
605
+ "group_id" => group_id
606
+ }
607
+ scrape = http_post(page, form)
608
+
609
+ restr = ''
610
+ #restr << Regexp.escape( package )
611
+ #restr << '\s*'
612
+ restr << Regexp.escape( '<a href="/frs/download.php/' )
613
+ restr << '(\d+)'
614
+ restr << Regexp.escape( %{/#{file}} )
615
+ re = Regexp.new(restr)
616
+
617
+ md = re.match(scrape)
618
+ if md
619
+ file_id = md[1]
620
+ end
621
+
622
+ @file_ids[[file, package]] = file_id
623
+ end
624
+
625
+ # Remove file from release.
626
+
627
+ def remove_file(file_id, release_id, package_id)
628
+ page="/frs/admin/editrelease.php"
629
+
630
+ form = {
631
+ "group_id" => group_id,
632
+ "package_id" => package_id,
633
+ "release_id" => release_id,
634
+ "file_id" => file_id,
635
+ "step3" => "Delete File",
636
+ "im_sure" => '1',
637
+ "submit" => "Delete File "
638
+ }
639
+
640
+ http_post(page, form)
641
+ end
642
+
643
+ #
644
+ # Add file to release.
645
+ #
646
+
647
+ def add_file(file, release_id, package_id, processor=nil)
648
+ page = '/frs/admin/editrelease.php'
649
+
650
+ userfile = open file, 'rb'
651
+
652
+ type_id = userfile.path[%r|\.[^\./]+$|]
653
+ type_id = FILETYPES[type_id]
654
+ processor_id = PROCESSORS[processor.downcase]
655
+
656
+ form = {
657
+ "step2" => '1',
658
+ "group_id" => group_id,
659
+ "package_id" => package_id,
660
+ "release_id" => release_id,
661
+ "userfile" => userfile,
662
+ "type_id" => type_id,
663
+ "processor_id" => processor_id,
664
+ "submit" => "Add This File"
665
+ }
666
+
667
+ boundary = Array::new(8){ "%2.2d" % rand(42) }.join('__')
668
+ boundary = "multipart/form-data; boundary=___#{ boundary }___"
669
+
670
+ http_post(page, form, 'content-type' => boundary)
671
+ end
672
+
673
+ # Posts news item to +group_id+ (can be name) with +subject+ and +body+
674
+
675
+ def post_news(subject, body)
676
+ page = "/news/submit.php"
677
+
678
+ subject % [unixname, version]
679
+
680
+ form = {
681
+ "group_id" => group_id,
682
+ "post_changes" => "y",
683
+ "summary" => subject,
684
+ "details" => body,
685
+ "submit" => "Submit"
686
+ }
687
+
688
+ login do
689
+ http_post(page, form)
690
+ end
691
+ end
692
+
693
+ # Constant for file types accepted by Rubyforge
694
+
695
+ FILETYPES = {
696
+ ".deb" => 1000,
697
+ ".rpm" => 2000,
698
+ ".zip" => 3000,
699
+ ".bz2" => 3100,
700
+ ".gz" => 3110,
701
+ ".src.zip" => 5000,
702
+ ".src.bz2" => 5010,
703
+ ".src.tar.bz2" => 5010,
704
+ ".src.gz" => 5020,
705
+ ".src.tar.gz" => 5020,
706
+ ".src.rpm" => 5100,
707
+ ".src" => 5900,
708
+ ".jpg" => 8000,
709
+ ".txt" => 8100,
710
+ ".text" => 8100,
711
+ ".htm" => 8200,
712
+ ".html" => 8200,
713
+ ".pdf" => 8300,
714
+ ".oth" => 9999,
715
+ ".ebuild" => 1300,
716
+ ".exe" => 1100,
717
+ ".dmg" => 1200,
718
+ ".tar.gz" => 3110,
719
+ ".tgz" => 3110,
720
+ ".gem" => 1400,
721
+ ".pgp" => 8150,
722
+ ".sig" => 8150
723
+ }
724
+
725
+ # Constant for processor types accepted by Rubyforge
726
+
727
+ PROCESSORS = {
728
+ "i386" => 1000,
729
+ "IA64" => 6000,
730
+ "Alpha" => 7000,
731
+ "Any" => 8000,
732
+ "PPC" => 2000,
733
+ "MIPS" => 3000,
734
+ "Sparc" => 4000,
735
+ "UltraSparc" => 5000,
736
+ "Other" => 9999,
737
+
738
+ "i386" => 1000,
739
+ "ia64" => 6000,
740
+ "alpha" => 7000,
741
+ "any" => 8000,
742
+ "ppc" => 2000,
743
+ "mips" => 3000,
744
+ "sparc" => 4000,
745
+ "ultrasparc" => 5000,
746
+ "other" => 9999,
747
+
748
+ "all" => 8000,
749
+ nil => 8000
750
+ }
751
+
752
+ end
753
+
754
+ end
755
+