1
2
3
4 require 'open-uri'
5 require 'fileutils'
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 class Updater
21
22 def zip_extract(file,target_dir)
23 system(
24 "unzip", "-q", file, "-d", target_dir
25 ) or raise "Error extracting (#{$?}): unzip #{file.inspect} -d #{target_dir.inspect}"
26 end
27
28 def initialize(wow_path,opts,listing_url="http://files.wowace.com/")
29 @opts=opts
30 @wow_path = wow_path
31 @addons_dir = File.join(@wow_path,"Interface","Addons")
32 @addons_download_dir = File.join(@wow_path,"Interface","ace_updater")
33 @listing_url=listing_url
34 end
35
36 def mod_index
37 @mod_index ||= begin
38 puts "Fetching Ace mod index from #{@listing_url}..."
39 m={}
40 open(@listing_url) {|f|
41 f.read
42 }.scan(
43 %r{<td>\s*<a href="([^"]+)">([^>]+)</a>\s*</td>\s*<td>r(\d+)</td>}
44 ) {|url,mod,revision|
45 m[mod] = [revision.to_i,url]
46 }
47 puts "#{m.length} entries found."
48 puts
49 m
50 end
51 end
52
53 def each_installed_mod
54 Dir.foreach(@addons_dir) {|addon|
55 next if ["..","."].include? addon
56 yield addon,installed_revision(addon)
57 }
58 end
59
60 def installed_revision(addon)
61 addon_dir = File.join(@addons_dir,addon)
62 x = Dir.glob(File.join(addon_dir,"Changelog-#{addon}-r*.xml"))
63 return nil if x.empty?
64 x[0] =~ /Changelog-#{addon}-r(\d+)\.xml/
65 $1.to_i
66 end
67
68 def get_changes(changelog,local_revision)
69 changes=[]
70 current_rev = "?????"
71 reading_changes = false
72 File.open(changelog) {|f|
73 f.read
74 }.each_line{|l|
75 l.strip!
76 if l =~ /^r(\d+) \|/
77 current_rev = $1.to_i
78 break if current_rev <= local_revision
79 elsif l=~/^-+$/
80 reading_changes = false
81 elsif l=~/^$/
82 reading_changes = true
83 else
84 changes << [current_rev,l] if reading_changes
85 end
86 }
87 changes.stable_sort_by {|r,t| r}
88 end
89
90 def update_mods(list=nil)
91 list=nil if list.empty?
92
93 remote_mods = mod_index()
94
95 puts "Scanning #{list ? 'chosen' : 'all'} mods for updates..."
96 jobs = []
97 each_installed_mod {|mod,revision|
98 next if revision.nil?
99 next if list and not list.include?(mod)
100
101 remote_revision,url = remote_mods[mod]
102 if remote_revision.nil? or remote_revision < revision
103 nonfatal("#{mod}: local is newer than remote (r#{revision.inspect} vs r#{remote_revision.inspect}). Skipping.")
104 elsif remote_revision > revision
105 jobs << [mod,revision,remote_revision,url]
106 end
107
108 list.delete mod if list
109 }
110 list.each {|x|
111 $stderr.puts "WARNING: #{x} is not an installed Ace addon, skipping update."
112 } if list
113
114 if jobs.length == 0
115 puts "#{list ? 'Chosen' : 'All'} mods up to date."
116 else
117 install_mods(jobs)
118 end
119 end
120
121 def add_mods(modlist)
122 remote_mods = mod_index()
123 jobs = modlist.map {|mod|
124 if remote_mods.key?(mod)
125 rev,url = remote_mods[mod]
126 [mod,nil,rev,url]
127 else
128 $stderr.puts "WARNING: #{mod} not found in mod index, skipping"
129 end
130 }.compact
131 install_mods(jobs)
132 end
133
134 def delete_mods(modlist)
135 modlist = modlist.map {|x|
136 next if ['.','..'].include?(x)
137 rev = installed_revision(x)
138 if rev
139 [x,rev]
140 else
141 nonfatal("#{x} is not an installed Ace addon, skipping")
142 end
143 }.compact
144
145 modlist.each_with_index {|(mod,rev),i|
146 status(i,modlist.length,"delete",mod,rev)
147 FileUtils.remove_dir(File.join(@addons_dir,mod))
148 }
149 end
150
151 def install_mods(jobs)
152 FileUtils.remove_dir(@addons_download_dir) if File.directory?(@addons_download_dir)
153 Dir.mkdir(@addons_download_dir)
154 jobs.each_with_index{|(mod,revision,remote_revision,url),index|
155 status(index, jobs.length, (revision ? 'update' : 'add'), mod, *([revision,remote_revision].compact))
156
157 zipfile = File.join(@addons_download_dir,"#{mod}.zip")
158
159 File.open(zipfile,'w') {|f|
160 open(url) {|data|
161 FileUtils.copy_stream(data,f)
162 }
163 }
164
165 zip_extract(zipfile,@addons_download_dir)
166 expected_output_dir = File.join(@addons_download_dir,mod)
167 destination_dir = File.join(@addons_dir,mod)
168 backup_dir = "#{destination_dir}.backup"
169
170 unless File.directory?(expected_output_dir)
171 nonfatal("#{expected_output_dir} not created as expected")
172 nonfatal("Check #{zipfile}")
173 nonfatal("Skipping installation of #{mod}")
174 end
175
176 changelog = File.join(expected_output_dir,"Changelog-#{mod}-r#{remote_revision}.xml")
177
178 FileUtils.chmod 0644,changelog
179 if revision and @opts.include?('-v')
180 changes = get_changes(changelog,revision)
181 changes.each {|revision,text|
182 puts(" * [r%d] %s" % [revision,text])
183 }
184 end
185
186 FileUtils.remove(zipfile)
187 FileUtils.mv(destination_dir,backup_dir) if revision
188 FileUtils.mv(expected_output_dir,destination_dir)
189 FileUtils.remove_dir(backup_dir) if revision
190 }
191 FileUtils.remove_dir(@addons_download_dir)
192 end
193
194 def status(sequence,total,action,mod,rev1,rev2=nil)
195 rev2 = " -> r#{rev2}" if rev2
196 puts("[%2d/%2d] %6s %16s r%d%s" % [sequence+1,total,action,mod,rev1,rev2])
197 end
198
199 def nonfatal(s)
200 $stderr.puts("WARNING: #{s}")
201 end
202 end
203
204 class Array
205 def stable_sort_by
206 n = 0
207 sort_by {|x| n+= 1; [yield(x), n]}
208 end
209 end
210
211
212 if __FILE__ == $0
213 opts,args = ARGV.partition {|text| text[0] == ?- }
214 args << 'update' if args.empty?
215 command = args.shift.downcase
216
217 updater = Updater.new(File.dirname(__FILE__),opts)
218
219 case command
220 when 'update'
221 updater.update_mods(args)
222 when 'add'
223 raise "Must specify mods to add" if args.empty?
224 updater.add_mods(args)
225 when 'delete'
226 raise "Must specify mods to delete" if args.empty?
227 updater.delete_mods(args)
228 else
229 puts "Valid commands: update add delete"
230 end
231 end
232