123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502 |
- # Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
- require 'rexml/document'
- class Package < ApplicationModel
- @@root = Rails.root.to_s
- # build package based on .szpm
- # Package.build(
- # :file => 'package.szpm',
- # :root => '/path/to/src/extention/',
- # :output => '/path/to/package_location/'
- # )
- def self.build(data)
- if data[:file]
- xml = self._read_file( data[:file], data[:root] || true )
- package = self._parse(xml)
- elsif data[:string]
- package = self._parse( data[:string] )
- end
- build_date = REXML::Element.new("build_date")
- build_date.text = Time.now.utc.iso8601
- build_host = REXML::Element.new("build_host")
- build_host.text = Socket.gethostname
- package.root.insert_after( '//zpm/description', build_date )
- package.root.insert_after( '//zpm/description', build_host )
- package.elements.each('zpm/filelist/file') do |element|
- location = element.attributes['location']
- content = self._read_file( location, data[:root] )
- base64 = Base64.encode64(content)
- element.text = base64
- end
- if data[:output]
- location = data[:output] + '/' + package.elements["zpm/name"].text + '-' + package.elements["zpm/version"].text + '.zpm'
- puts "NOTICE: writting package to '#{location}'"
- file = File.new( location, 'wb' )
- file.write( package.to_s )
- file.close
- return true
- end
- return package.to_s
- end
- # Package.auto_install
- # install all packages located under auto_install/*.zpm
- def self.auto_install
- path = @@root + '/auto_install/'
- return if ! File.exist?( path )
- data = []
- Dir.foreach( path ) do |entry|
- if entry =~ /\.zpm/
- data.push entry
- end
- end
- data.each {|file|
- self.install( :file => path + '/' + file )
- }
- return data
- end
- # Package.unlink_all
- # remove all linked files in application
- # note: will not take down package migrations, use Package.unlink instead
- def self.unlink_all
- # link files
- Dir.glob( @@root + '/**/*' ) do |entry|
- if File.symlink?( entry)
- puts "unlink: #{entry}"
- File.delete( entry )
- end
- backup_file = entry + '.link_backup'
- if File.exists?( backup_file )
- puts "Restore backup file of #{backup_file} -> #{entry}."
- File.rename( backup_file, entry )
- end
- end
- end
- # check if zpm is a package source repo
- def self._package_base_dir?(package_base_dir)
- package = false
- Dir.glob( package_base_dir + '/*.szpm') do |entry|
- package = entry.sub( /^.*\/(.+?)\.szpm$/, '\1')
- end
- if package == false
- raise "Can't link package, '#{package_base_dir}' is no package source directory!"
- end
- puts package.inspect
- return package
- end
- # Package.unlink('/path/to/src/extention')
- # execute migration down + unlink files
- def self.unlink(package_base_dir)
- # check if zpm is a package source repo
- package = self._package_base_dir?(package_base_dir)
- # migration down
- Package::Migration.migrate( package, 'reverse' )
- # link files
- Dir.glob( package_base_dir + '/**/*' ) do |entry|
- entry = entry.sub( '//', '/' )
- file = entry
- file = file.sub( /#{package_base_dir.to_s}/, '' )
- dest = @@root + '/' + file
- if File.symlink?( dest.to_s )
- puts "Unlink file: #{dest.to_s}"
- File.delete( dest.to_s )
- end
- backup_file = dest.to_s + '.link_backup'
- if File.exists?( backup_file )
- puts "Restore backup file of #{backup_file} -> #{dest.to_s}."
- File.rename( backup_file, dest.to_s )
- end
- end
- end
- # Package.link('/path/to/src/extention')
- # link files + execute migration up
- def self.link(package_base_dir)
- # check if zpm is a package source repo
- package = self._package_base_dir?(package_base_dir)
- # link files
- Dir.glob( package_base_dir + '/**/*' ) do |entry|
- entry = entry.sub( '//', '/' )
- file = entry
- file = file.sub( /#{package_base_dir.to_s}/, '' )
- file = file.sub( /^\//, '' )
- # ignore files
- if file =~ /^README/
- puts "NOTICE: Ignore #{file}"
- next
- end
- # get new file destination
- dest = @@root + '/' + file
- if File.directory?( entry.to_s )
- if !File.exists?( dest.to_s )
- puts "Create dir: #{dest.to_s}"
- FileUtils.mkdir_p( dest.to_s )
- end
- end
- if File.file?( entry.to_s ) && ( File.file?( dest.to_s ) && !File.symlink?( dest.to_s ) )
- backup_file = dest.to_s + '.link_backup'
- if File.exists?( backup_file )
- raise "Can't link #{entry.to_s} -> #{dest.to_s}, destination and .link_backup already exists!"
- else
- puts "Create backup file of #{dest.to_s} -> #{backup_file}."
- File.rename( dest.to_s, backup_file )
- end
- end
- if File.file?( entry )
- if File.symlink?( dest.to_s )
- File.delete( dest.to_s )
- end
- puts "Link file: #{entry.to_s} -> #{dest.to_s}"
- File.symlink( entry.to_s, dest.to_s )
- end
- end
- # migration up
- Package::Migration.migrate( package )
- end
- # Package.install( :file => '/path/to/package.zpm' )
- # Package.install( :string => zpm_as_string )
- def self.install(data)
- if data[:file]
- xml = self._read_file( data[:file], true )
- package = self._parse(xml)
- elsif data[:string]
- package = self._parse( data[:string] )
- end
- # package meta data
- meta = {
- :name => package.elements["zpm/name"].text,
- :version => package.elements["zpm/version"].text,
- :vendor => package.elements["zpm/vendor"].text,
- :state => 'uninstalled',
- :created_by_id => 1,
- :updated_by_id => 1,
- }
- # verify if package can get installed
- package_db = Package.where( :name => meta[:name] ).first
- if package_db
- if !data[:reinstall]
- if Gem::Version.new( package_db.version ) == Gem::Version.new( meta[:version] )
- raise "Package '#{meta[:name]}-#{meta[:version]}' already installed!"
- end
- if Gem::Version.new( package_db.version ) > Gem::Version.new( meta[:version] )
- raise "Newer version (#{package_db.version}) of package '#{meta[:name]}-#{meta[:version]}' already installed!"
- end
- end
- # uninstall files of old package
- self.uninstall({
- :name => package_db.name,
- :version => package_db.version,
- :migration_not_down => true,
- })
- end
- # store package
- record = Package.create( meta )
- if !data[:reinstall]
- Store.add(
- :object => 'Package',
- :o_id => record.id,
- :data => package.to_s,
- :filename => meta[:name] + '-' + meta[:version] + '.zpm',
- :preferences => {},
- :created_by_id => UserInfo.current_user_id || 1,
- )
- end
- # write files
- package.elements.each('zpm/filelist/file') do |element|
- location = element.attributes['location']
- permission = element.attributes['permission'] || '644'
- base64 = element.text
- content = Base64.decode64(base64)
- content = self._write_file(location, permission, content)
- end
- # update package state
- record.state = 'installed'
- record.save
- # reload new files
- Package.reload_classes
- # up migrations
- Package::Migration.migrate( meta[:name] )
- # prebuild assets
- return true
- end
- # Package.reinstall( package_name )
- def self.reinstall(package_name)
- package = Package.where( :name => package_name ).first
- if !package
- raise "No such package '#{package_name}'"
- end
- file = self._get_bin( package.name, package.version )
- self.install( :string => file, :reinstall => true )
- end
- # Package.uninstall( :name => 'package', :version => '0.1.1' )
- # Package.uninstall( :string => zpm_as_string )
- def self.uninstall( data )
- if data[:string]
- package = self._parse( data[:string] )
- else
- file = self._get_bin( data[:name], data[:version] )
- package = self._parse(file)
- end
- # package meta data
- meta = {
- :name => package.elements["zpm/name"].text,
- :version => package.elements["zpm/version"].text,
- }
- # down migrations
- if !data[:migration_not_down]
- Package::Migration.migrate( meta[:name], 'reverse' )
- end
- package.elements.each('zpm/filelist/file') do |element|
- location = element.attributes['location']
- permission = element.attributes['permission'] || '644'
- base64 = element.text
- content = Base64.decode64(base64)
- content = self._delete_file(location, permission, content)
- end
- # prebuild assets
- # reload new files
- Package.reload_classes
- # delete package
- record = Package.where(
- :name => meta[:name],
- :version => meta[:version],
- ).first
- record.destroy
- return true
- end
- # reload .rb files in case they have changed
- def self.reload_classes
- ['app', 'lib'].each {|dir|
- Dir.glob( Rails.root.join( dir + '/**/*') ).each {|entry|
- if entry =~ /\.rb$/
- begin
- load entry
- rescue => e
- puts 'ERROR: ' + e.inspect
- puts 'Traceback: ' + e.backtrace
- end
- end
- }
- }
- end
- def self._parse(xml)
- # puts xml.inspect
- begin
- package = REXML::Document.new( xml )
- rescue => e
- puts 'ERROR: ' + e.inspect
- return
- end
- # puts package.inspect
- return package
- end
- def self._get_bin( name, version )
- package = Package.where(
- :name => name,
- :version => version,
- ).first
- if !package
- raise "No such package '#{name}' version '#{version}'"
- end
- list = Store.list(
- :object => 'Package',
- :o_id => package.id,
- )
- # find file
- if !list || !list.first
- raise "No such file in storage list #{name} #{version}"
- end
- store_file = list.first.store_file
- if !store_file
- raise "No such file in storage #{name} #{version}"
- end
- store_file.data
- end
- def self._read_file(file, fullpath = false)
- if fullpath == false
- location = @@root + '/' + file
- elsif fullpath == true
- location = file
- else
- location = fullpath + '/' + file
- end
- begin
- data = File.open( location, 'rb' )
- contents = data.read
- rescue => e
- raise 'ERROR: ' + e.inspect
- end
- return contents
- end
- def self._write_file(file, permission, data)
- location = @@root + '/' + file
- # rename existing file
- if File.exist?( location )
- backup_location = location + '.save'
- puts "NOTICE: backup old file '#{location}' to #{backup_location}"
- File.rename( location, backup_location )
- end
- # check if directories need to be created
- directories = location.split '/'
- (0..(directories.length-2) ).each {|position|
- tmp_path = ''
- (1..position).each {|count|
- tmp_path = tmp_path + '/' + directories[count].to_s
- }
- if tmp_path != ''
- if !File.exist?(tmp_path)
- Dir.mkdir( tmp_path, 0755)
- end
- end
- }
- # install file
- begin
- puts "NOTICE: install '#{location}' (#{permission})"
- file = File.new( location, 'wb' )
- file.write( data )
- file.close
- File.chmod( permission.to_i(8), location )
- rescue => e
- raise 'ERROR: ' + e.inspect
- end
- return true
- end
- def self._delete_file(file, permission, data)
- location = @@root + '/' + file
- # install file
- puts "NOTICE: uninstall '#{location}'"
- if File.exist?( location )
- File.delete( location )
- end
- # rename existing file
- backup_location = location + '.save'
- if File.exist?( backup_location )
- puts "NOTICE: restore old file '#{backup_location}' to #{location}"
- File.rename( backup_location, location )
- end
- return true
- end
- class Migration < ApplicationModel
- @@root = Rails.root.to_s
- def self.migrate( package, direction = 'normal' )
- location = @@root + '/db/addon/' + package.underscore
- return true if !File.exists?( location )
- migrations_done = Package::Migration.where( :name => package.underscore )
- # get existing migrations
- migrations_existing = []
- Dir.foreach(location) {|entry|
- next if entry == '.'
- next if entry == '..'
- migrations_existing.push entry
- }
- # up
- migrations_existing = migrations_existing.sort
- # down
- if direction == 'reverse'
- migrations_existing = migrations_existing.reverse
- end
- migrations_existing.each {|migration|
- next if migration !~ /\.rb$/
- version = nil
- name = nil
- if migration =~ /^(.+?)_(.*)\.rb$/
- version = $1
- name = $2
- end
- if !version || !name
- raise "Invalid package migration '#{migration}'"
- end
- # down
- if direction == 'reverse'
- done = Package::Migration.where( :name => package.underscore, :version => version ).first
- next if !done
- puts "NOTICE: down package migration '#{migration}'"
- load "#{location}/#{migration}"
- classname = name.camelcase
- Kernel.const_get(classname).down
- record = Package::Migration.where( :name => package.underscore, :version => version ).first
- if record
- record.destroy
- end
- # up
- else
- done = Package::Migration.where( :name => package.underscore, :version => version ).first
- next if done
- puts "NOTICE: up package migration '#{migration}'"
- load "#{location}/#{migration}"
- classname = name.camelcase
- Kernel.const_get(classname).up
- Package::Migration.create( :name => package.underscore, :version => version )
- end
- # reload new files
- Package.reload_classes
- }
- end
- end
- end
|