package.rb 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. require 'rexml/document'
  2. class Package < ApplicationModel
  3. @@root = Rails.root.to_s
  4. # build package based on .szpm
  5. # Package.build(
  6. # :file => 'package.szpm',
  7. # :root => '/path/to/src/extention/',
  8. # :output => '/path/to/package_location/'
  9. # )
  10. def self.build(data)
  11. if data[:file]
  12. xml = self._read_file( data[:file], data[:root] || true )
  13. package = self._parse(xml)
  14. elsif data[:string]
  15. package = self._parse( data[:string] )
  16. end
  17. build_date = REXML::Element.new("build_date")
  18. build_date.text = Time.now.utc.iso8601
  19. build_host = REXML::Element.new("build_host")
  20. build_host.text = Socket.gethostname
  21. package.root.insert_after( '//zpm/description', build_date )
  22. package.root.insert_after( '//zpm/description', build_host )
  23. package.elements.each('zpm/filelist/file') do |element|
  24. location = element.attributes['location']
  25. content = self._read_file( location, data[:root] )
  26. base64 = Base64.encode64(content)
  27. element.text = base64
  28. end
  29. if data[:output]
  30. location = data[:output] + '/' + package.elements["zpm/name"].text + '-' + package.elements["zpm/version"].text + '.zpm'
  31. puts "NOTICE: writting package to '#{location}'"
  32. file = File.new( location, 'wb' )
  33. file.write( package.to_s )
  34. file.close
  35. return true
  36. end
  37. return package.to_s
  38. end
  39. # Package.auto_install
  40. # install all packages located under auto_install/*.zpm
  41. def self.auto_install
  42. path = @@root + '/auto_install/'
  43. return if ! File.exist?( path )
  44. data = []
  45. Dir.foreach( path ) do |entry|
  46. if entry =~ /\.zpm/
  47. data.push entry
  48. end
  49. end
  50. data.each {|file|
  51. self.install( :file => path + '/' + file )
  52. }
  53. return data
  54. end
  55. # Package.unlink_all
  56. # remove all linked files in application
  57. # note: will not take down package migrations, use Package.unlink instead
  58. def self.unlink_all
  59. # link files
  60. Dir.glob( @@root + '/**/*' ) do |entry|
  61. if File.symlink?( entry)
  62. puts "unlink: #{entry}"
  63. File.delete( entry )
  64. end
  65. backup_file = entry + '.link_backup'
  66. if File.exists?( backup_file )
  67. puts "Restore backup file of #{backup_file} -> #{entry}."
  68. File.rename( backup_file, entry )
  69. end
  70. end
  71. end
  72. # check if zpm is a package source repo
  73. def self._package_base_dir?(package_base_dir)
  74. package = false
  75. Dir.glob( package_base_dir + '/*.szpm') do |entry|
  76. package = entry.sub( /^.*\/(.+?)\.szpm$/, '\1')
  77. end
  78. if package == false
  79. raise "Can't link package, '#{package_base_dir}' is no package source directory!"
  80. end
  81. puts package.inspect
  82. return package
  83. end
  84. # Package.unlink('/path/to/src/extention')
  85. # execute migration down + unlink files
  86. def self.unlink(package_base_dir)
  87. # check if zpm is a package source repo
  88. package = self._package_base_dir?(package_base_dir)
  89. # migration down
  90. Package::Migration.migrate( package, 'reverse' )
  91. # link files
  92. Dir.glob( package_base_dir + '/**/*' ) do |entry|
  93. entry = entry.sub( '//', '/' )
  94. file = entry
  95. file = file.sub( /#{package_base_dir.to_s}/, '' )
  96. dest = @@root + '/' + file
  97. if File.symlink?( dest.to_s )
  98. puts "Unlink file: #{dest.to_s}"
  99. File.delete( dest.to_s )
  100. end
  101. backup_file = dest.to_s + '.link_backup'
  102. if File.exists?( backup_file )
  103. puts "Restore backup file of #{backup_file} -> #{dest.to_s}."
  104. File.rename( backup_file, dest.to_s )
  105. end
  106. end
  107. end
  108. # Package.link('/path/to/src/extention')
  109. # link files + execute migration up
  110. def self.link(package_base_dir)
  111. # check if zpm is a package source repo
  112. package = self._package_base_dir?(package_base_dir)
  113. # link files
  114. Dir.glob( package_base_dir + '/**/*' ) do |entry|
  115. entry = entry.sub( '//', '/' )
  116. file = entry
  117. file = file.sub( /#{package_base_dir.to_s}/, '' )
  118. file = file.sub( /^\//, '' )
  119. # ignore files
  120. if file =~ /^README/
  121. puts "NOTICE: Ignore #{file}"
  122. next
  123. end
  124. # get new file destination
  125. dest = @@root + '/' + file
  126. if File.directory?( entry.to_s )
  127. if !File.exists?( dest.to_s )
  128. puts "Create dir: #{dest.to_s}"
  129. FileUtils.mkdir_p( dest.to_s )
  130. end
  131. end
  132. if File.file?( entry.to_s ) && ( File.file?( dest.to_s ) && !File.symlink?( dest.to_s ) )
  133. backup_file = dest.to_s + '.link_backup'
  134. if File.exists?( backup_file )
  135. raise "Can't link #{entry.to_s} -> #{dest.to_s}, destination and .link_backup already exists!"
  136. else
  137. puts "Create backup file of #{dest.to_s} -> #{backup_file}."
  138. File.rename( dest.to_s, backup_file )
  139. end
  140. end
  141. if File.file?( entry )
  142. if File.symlink?( dest.to_s )
  143. File.delete( dest.to_s )
  144. end
  145. puts "Link file: #{entry.to_s} -> #{dest.to_s}"
  146. File.symlink( entry.to_s, dest.to_s )
  147. end
  148. end
  149. # migration up
  150. Package::Migration.migrate( package )
  151. end
  152. # Package.install( :file => '/path/to/package.zpm' )
  153. # Package.install( :string => zpm_as_string )
  154. def self.install(data)
  155. if data[:file]
  156. xml = self._read_file( data[:file], true )
  157. package = self._parse(xml)
  158. elsif data[:string]
  159. package = self._parse( data[:string] )
  160. end
  161. # package meta data
  162. meta = {
  163. :name => package.elements["zpm/name"].text,
  164. :version => package.elements["zpm/version"].text,
  165. :vendor => package.elements["zpm/vendor"].text,
  166. :state => 'uninstalled',
  167. :created_by_id => 1,
  168. :updated_by_id => 1,
  169. }
  170. # verify if package can get installed
  171. package_db = Package.where( :name => meta[:name] ).first
  172. if package_db
  173. if !data[:reinstall]
  174. if Gem::Version.new( package_db.version ) == Gem::Version.new( meta[:version] )
  175. raise "Package '#{meta[:name]}-#{meta[:version]}' already installed!"
  176. end
  177. if Gem::Version.new( package_db.version ) > Gem::Version.new( meta[:version] )
  178. raise "Newer version (#{package_db.version}) of package '#{meta[:name]}-#{meta[:version]}' already installed!"
  179. end
  180. end
  181. # uninstall files of old package
  182. self.uninstall({
  183. :name => package_db.name,
  184. :version => package_db.version,
  185. :migration_not_down => true,
  186. })
  187. end
  188. # store package
  189. record = Package.create( meta )
  190. if !data[:reinstall]
  191. Store.add(
  192. :object => 'Package',
  193. :o_id => record.id,
  194. :data => package.to_s,
  195. :filename => meta[:name] + '-' + meta[:version] + '.zpm',
  196. :preferences => {},
  197. :created_by_id => UserInfo.current_user_id || 1,
  198. )
  199. end
  200. # write files
  201. package.elements.each('zpm/filelist/file') do |element|
  202. location = element.attributes['location']
  203. permission = element.attributes['permission'] || '644'
  204. base64 = element.text
  205. content = Base64.decode64(base64)
  206. content = self._write_file(location, permission, content)
  207. end
  208. # update package state
  209. record.state = 'installed'
  210. record.save
  211. # reload new files
  212. Package.reload_classes
  213. # up migrations
  214. Package::Migration.migrate( meta[:name] )
  215. # prebuild assets
  216. return true
  217. end
  218. # Package.reinstall( package_name )
  219. def self.reinstall(package_name)
  220. package = Package.where( :name => package_name ).first
  221. return if !package
  222. file = self._get_bin( package.name, package.version )
  223. return if !file
  224. self.install( :string => file, :reinstall => true )
  225. end
  226. # Package.uninstall( :name => 'package', :version => '0.1.1' )
  227. # Package.uninstall( :string => zpm_as_string )
  228. def self.uninstall( data )
  229. if data[:string]
  230. package = self._parse( data[:string] )
  231. else
  232. file = self._get_bin( data[:name], data[:version] )
  233. package = self._parse(file)
  234. end
  235. # package meta data
  236. meta = {
  237. :name => package.elements["zpm/name"].text,
  238. :version => package.elements["zpm/version"].text,
  239. }
  240. # down migrations
  241. if !data[:migration_not_down]
  242. Package::Migration.migrate( meta[:name], 'reverse' )
  243. end
  244. package.elements.each('zpm/filelist/file') do |element|
  245. location = element.attributes['location']
  246. permission = element.attributes['permission'] || '644'
  247. base64 = element.text
  248. content = Base64.decode64(base64)
  249. content = self._delete_file(location, permission, content)
  250. end
  251. # prebuild assets
  252. # reload new files
  253. Package.reload_classes
  254. # delete package
  255. record = Package.where(
  256. :name => meta[:name],
  257. :version => meta[:version],
  258. ).first
  259. record.destroy
  260. return true
  261. end
  262. # reload .rb files in case they have changed
  263. def self.reload_classes
  264. ['app', 'lib'].each {|dir|
  265. Dir.glob( Rails.root.join( dir + '/**/*') ).each {|entry|
  266. if entry =~ /\.rb$/
  267. begin
  268. load entry
  269. rescue => e
  270. puts 'ERROR: ' + e.inspect
  271. end
  272. end
  273. }
  274. }
  275. end
  276. def self._parse(xml)
  277. # puts xml.inspect
  278. begin
  279. package = REXML::Document.new( xml )
  280. rescue => e
  281. puts 'ERROR: ' + e.inspect
  282. return
  283. end
  284. # puts package.inspect
  285. return package
  286. end
  287. def self._get_bin( name, version )
  288. package = Package.where(
  289. :name => name,
  290. :version => version,
  291. ).first
  292. if !package
  293. raise "No such package '#{name}' version '#{version}'"
  294. end
  295. list = Store.list(
  296. :object => 'Package',
  297. :o_id => package.id,
  298. )
  299. # find file
  300. return if !list
  301. list.first.store_file.data
  302. end
  303. def self._read_file(file, fullpath = false)
  304. if fullpath == false
  305. location = @@root + '/' + file
  306. elsif fullpath == true
  307. location = file
  308. else
  309. location = fullpath + '/' + file
  310. end
  311. begin
  312. data = File.open( location, 'rb' )
  313. contents = data.read
  314. rescue => e
  315. raise 'ERROR: ' + e.inspect
  316. end
  317. return contents
  318. end
  319. def self._write_file(file, permission, data)
  320. location = @@root + '/' + file
  321. # rename existing file
  322. if File.exist?( location )
  323. backup_location = location + '.save'
  324. puts "NOTICE: backup old file '#{location}' to #{backup_location}"
  325. File.rename( location, backup_location )
  326. end
  327. # check if directories need to be created
  328. directories = location.split '/'
  329. (0..(directories.length-2) ).each {|position|
  330. tmp_path = ''
  331. (1..position).each {|count|
  332. tmp_path = tmp_path + '/' + directories[count].to_s
  333. }
  334. if tmp_path != ''
  335. if !File.exist?(tmp_path)
  336. Dir.mkdir( tmp_path, 0755)
  337. end
  338. end
  339. }
  340. # install file
  341. begin
  342. puts "NOTICE: install '#{location}' (#{permission})"
  343. file = File.new( location, 'wb' )
  344. file.write( data )
  345. file.close
  346. File.chmod( permission.to_i(8), location )
  347. rescue => e
  348. raise 'ERROR: ' + e.inspect
  349. end
  350. return true
  351. end
  352. def self._delete_file(file, permission, data)
  353. location = @@root + '/' + file
  354. # install file
  355. puts "NOTICE: uninstall '#{location}'"
  356. if File.exist?( location )
  357. File.delete( location )
  358. end
  359. # rename existing file
  360. backup_location = location + '.save'
  361. if File.exist?( backup_location )
  362. puts "NOTICE: restore old file '#{backup_location}' to #{location}"
  363. File.rename( backup_location, location )
  364. end
  365. return true
  366. end
  367. class Migration < ApplicationModel
  368. @@root = Rails.root.to_s
  369. def self.migrate( package, direction = 'normal' )
  370. location = @@root + '/db/addon/' + package.underscore
  371. return true if !File.exists?( location )
  372. migrations_done = Package::Migration.where( :name => package.underscore )
  373. # get existing migrations
  374. migrations_existing = []
  375. Dir.foreach(location) {|entry|
  376. next if entry == '.'
  377. next if entry == '..'
  378. migrations_existing.push entry
  379. }
  380. # up
  381. migrations_existing = migrations_existing.sort
  382. # down
  383. if direction == 'reverse'
  384. migrations_existing = migrations_existing.reverse
  385. end
  386. migrations_existing.each {|migration|
  387. next if migration !~ /\.rb$/
  388. version = nil
  389. name = nil
  390. if migration =~ /^(.+?)_(.*)\.rb$/
  391. version = $1
  392. name = $2
  393. end
  394. if !version || !name
  395. raise "Invalid package migration '#{migration}'"
  396. end
  397. # down
  398. if direction == 'reverse'
  399. done = Package::Migration.where( :name => package.underscore, :version => version ).first
  400. next if !done
  401. puts "NOTICE: down package migration '#{migration}'"
  402. load "#{location}/#{migration}"
  403. classname = name.camelcase
  404. Kernel.const_get(classname).down
  405. record = Package::Migration.where( :name => package.underscore, :version => version ).first
  406. if record
  407. record.destroy
  408. end
  409. # up
  410. else
  411. done = Package::Migration.where( :name => package.underscore, :version => version ).first
  412. next if done
  413. puts "NOTICE: up package migration '#{migration}'"
  414. load "#{location}/#{migration}"
  415. classname = name.camelcase
  416. Kernel.const_get(classname).up
  417. Package::Migration.create( :name => package.underscore, :version => version )
  418. end
  419. # reload new files
  420. Package.reload_classes
  421. }
  422. end
  423. end
  424. end