package.rb 14 KB

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