rhype 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+