diff --git a/app/models/package.rb b/app/models/package.rb
new file mode 100644
index 000000000..3d1d8a0cd
--- /dev/null
+++ b/app/models/package.rb
@@ -0,0 +1,296 @@
+require 'rexml/document'
+class Package < ApplicationModel
+ @@root = Rails.root.to_s
+
+ def self.build_file(file)
+ xml = self._read_file(file, true)
+ package = self._parse(xml)
+ self.build(package)
+ end
+
+ def self.build(package)
+ package.elements.each('zpm/filelist/file') do |element|
+ location = element.attributes['location']
+ content = self._read_file(location)
+ base64 = Base64.encode64(content)
+ element.text = base64
+ end
+ return package.to_s
+ end
+
+ def self.install_file(file)
+ xml = self._read_file(file)
+ package = self._parse(xml)
+ self.install(package)
+ end
+
+ def self.install_string(xml)
+ package = self._parse(xml)
+ self.install(package)
+ end
+
+ def self.install(package)
+
+ # package meta data
+ data = {
+ :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 => data[:name] ).first
+ if package_db
+ if Gem::Version.new( package_db.version ) == Gem::Version.new( data[:version] )
+ raise "Package '#{data[:name]}' already installed!"
+ end
+ if Gem::Version.new( package_db.version ) > Gem::Version.new( data[:version] )
+ raise "Newer version (#{package_db.version}) of package '#{data[:name]}-#{data[:version]}' already installed!"
+ end
+
+ # uninstall old package
+ self.uninstall_name( package_db.name, package_db.version, false )
+ end
+
+ # store package
+ record = Package.create( data )
+ Store.add(
+ :object => 'Package',
+ :o_id => record.id,
+ :data => package.to_s,
+ :filename => data[:name] + '-' + data[:version] + '.zpm',
+ :preferences => {},
+ )
+
+ # 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
+
+ # up migrations
+ Package::Migration.migrate( data[:name] )
+
+ # prebuild assets
+
+ return true
+ end
+
+ def self.uninstall_name( name, version, migration_down = true )
+ file = self._get_bin( name, version )
+ package = self._parse(file)
+ self.uninstall( package, migration_down )
+ end
+
+ def self.uninstall_string(xml)
+ package = self._parse(xml)
+ self.uninstall(package)
+ end
+
+ def self.uninstall( package, migration_down = true )
+
+ # package meta data
+ data = {
+ :name => package.elements["zpm/name"].text,
+ :version => package.elements["zpm/version"].text,
+ }
+
+ # down migrations
+ if migration_down
+ Package::Migration.migrate( data[: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
+
+ # delete package
+ record = Package.where(
+ :name => data[:name],
+ :version => data[:version],
+ ).first
+ record.destroy
+
+ return true
+ 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
+ return if !list
+ list.first.store_file.data
+ end
+
+ def self._read_file(file, fullpath = false)
+ if !fullpath
+ location = @@root + '/' + file
+ else
+ location = 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}'"
+ File.delete( location )
+
+ # 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 )
+
+ # 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 => name, :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 => name, :version => version ).first
+ if record
+ record.destroy
+ end
+
+ # up
+ else
+ done = Package::Migration.where( :name => name, :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 => name, :version => version )
+ end
+ }
+ end
+ end
+end
\ No newline at end of file
diff --git a/db/migrate/20121225162100_create_package.rb b/db/migrate/20121225162100_create_package.rb
new file mode 100644
index 000000000..f20b83237
--- /dev/null
+++ b/db/migrate/20121225162100_create_package.rb
@@ -0,0 +1,23 @@
+class CreatePackage < ActiveRecord::Migration
+ def up
+ create_table :packages do |t|
+ t.column :name, :string, :limit => 250, :null => false
+ t.column :version, :string, :limit => 50, :null => false
+ t.column :vendor, :string, :limit => 150, :null => false
+ t.column :state, :string, :limit => 50, :null => false
+ t.column :updated_by_id, :integer, :null => false
+ t.column :created_by_id, :integer, :null => false
+ t.timestamps
+ end
+ create_table :package_migrations do |t|
+ t.column :name, :string, :limit => 250, :null => false
+ t.column :version, :string, :limit => 250, :null => false
+ t.timestamps
+ end
+ end
+
+ def down
+ drop_table :packages
+ drop_table :package_migrations
+ end
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index bb02ae838..665d7313b 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -13,6 +13,9 @@ class ActiveSupport::TestCase
SimpleCov.start
fixtures :all
+ # disable transactions
+ self.use_transactional_fixtures = false
+
# load seeds
load "#{Rails.root}/db/seeds.rb"
diff --git a/test/unit/package_test.rb b/test/unit/package_test.rb
new file mode 100644
index 000000000..5452a7e77
--- /dev/null
+++ b/test/unit/package_test.rb
@@ -0,0 +1,209 @@
+# encoding: utf-8
+require 'test_helper'
+
+class PackageTest < ActiveSupport::TestCase
+ test 'packages' do
+ tests = [
+
+ # test 1
+ {
+ :zpm => '
+
+ UnitTestSample
+ 1.0.1
+ Znuny GmbH
+ http://znuny.org/
+ ABC
+ some description
+
+ YWJjw6TDtsO8w58=
+ YWJjw6TDtsO8w58=
+ Y2xhc3MgQ3JlYXRlQmFzZSA8IEFjdGl2ZVJlY29yZDo6TWlncmF0aW9uDQogIGRlZiBzZWxmLnVw
+DQogICBjcmVhdGVfdGFibGUgOnNhbXBsZV90YWJsZXMgZG8gfHR8DQogICAgICB0LmNvbHVtbiA6
+bmFtZSwgICAgICAgICAgIDpzdHJpbmcsIDpsaW1pdCA9PiAxNTAsICA6bnVsbCA9PiB0cnVlDQog
+ICAgICB0LmNvbHVtbiA6ZGF0YSwgICAgICAgICAgIDpzdHJpbmcsIDpsaW1pdCA9PiA1MDAwLCA6
+bnVsbCA9PiB0cnVlDQogICAgZW5kDQogIGVuZA0KDQogIGRlZiBzZWxmLmRvd24NCiAgICBkcm9w
+X3RhYmxlIDpzYW1wbGVfdGFibGVzDQogIGVuZA0KZW5k
+
+',
+ :action => 'install',
+ :result => true,
+ :verify => {
+ :package => {
+ :name => 'UnitTestSample',
+ :version => '1.0.1',
+ },
+ :check_files => [
+ {
+ :location => 'test.txt',
+ :result => true,
+ },
+ {
+ :location => 'test2.txt',
+ :result => false,
+ },
+ {
+ :location => 'some/dir/test.txt',
+ :result => true,
+ },
+ ],
+ },
+ },
+
+ # test 2
+ {
+ :zpm => '
+
+ UnitTestSample
+ 1.0.1
+ Znuny GmbH
+ http://znuny.org/
+ ABC
+ some description
+
+ YWJjw6TDtsO8w58=
+
+',
+ :action => 'install',
+ :result => false,
+ },
+
+ # test 3
+ {
+ :zpm => '
+
+ UnitTestSample
+ 1.0.0
+ Znuny GmbH
+ http://znuny.org/
+ ABC
+ some description
+
+ YWJjw6TDtsO8w58=
+
+',
+ :action => 'install',
+ :result => false,
+ },
+
+ # test 4
+ {
+ :zpm => '
+
+ UnitTestSample
+ 1.0.2
+ Znuny GmbH
+ http://znuny.org/
+ ABC
+ some description
+
+ YWJjw6TDtsO8w58=
+ YWJjw6TDtsO8w58=
+ Y2xhc3MgQ3JlYXRlQmFzZSA8IEFjdGl2ZVJlY29yZDo6TWlncmF0aW9uDQogIGRlZiBzZWxmLnVw
+DQogICBjcmVhdGVfdGFibGUgOnNhbXBsZV90YWJsZXMgZG8gfHR8DQogICAgICB0LmNvbHVtbiA6
+bmFtZSwgICAgICAgICAgIDpzdHJpbmcsIDpsaW1pdCA9PiAxNTAsICA6bnVsbCA9PiB0cnVlDQog
+ICAgICB0LmNvbHVtbiA6ZGF0YSwgICAgICAgICAgIDpzdHJpbmcsIDpsaW1pdCA9PiA1MDAwLCA6
+bnVsbCA9PiB0cnVlDQogICAgZW5kDQogIGVuZA0KDQogIGRlZiBzZWxmLmRvd24NCiAgICBkcm9w
+X3RhYmxlIDpzYW1wbGVfdGFibGVzDQogIGVuZA0KZW5k
+
+',
+ :action => 'install',
+ :result => true,
+ :verify => {
+ :package => {
+ :name => 'UnitTestSample',
+ :version => '1.0.2',
+ },
+ :check_files => [
+ {
+ :location => 'test.txt2',
+ :result => true,
+ },
+ {
+ :location => 'test.txt',
+ :result => false,
+ },
+ {
+ :location => 'test2.txt',
+ :result => false,
+ },
+ {
+ :location => 'some/dir/test.txt2',
+ :result => true,
+ },
+ ],
+ },
+ },
+
+ # test 4
+ {
+ :name => 'UnitTestSample',
+ :version => '1.0.2',
+ :action => 'uninstall',
+ :result => true,
+ :verify => {
+ :check_files => [
+ {
+ :location => 'test.txt',
+ :result => false,
+ },
+ {
+ :location => 'test2.txt',
+ :result => false,
+ },
+ ],
+ },
+ },
+
+ ]
+ tests.each { |test|
+ if test[:action] == 'install'
+ begin
+ success = Package.install_string( test[:zpm] )
+ rescue => e
+ puts 'ERROR: ' + e.inspect
+ success = false
+ end
+ if test[:result]
+ assert( success, "install package not successful" )
+ else
+ assert( !success, "install package successful but should not" )
+ end
+ elsif test[:action] == 'uninstall'
+ if test[:zpm]
+ begin
+ success = Package.uninstall_string( test[:zpm] )
+ rescue
+ success = false
+ end
+ else
+ begin
+ success = Package.uninstall_name( test[:name], test[:version] )
+ rescue
+ success = false
+ end
+ end
+ if test[:result]
+ assert( success, "uninstall package not successful" )
+ else
+ assert( !success, "uninstall package successful but should not" )
+ end
+ end
+ if test[:verify] && test[:verify][:package]
+ exists = Package.where( :name => test[:verify][:package][:name], :version => test[:verify][:package][:version] ).first
+ assert( exists, "package '#{test[:verify][:package][:name]}' is not installed" )
+ end
+ if test[:verify] && test[:verify][:check_files]
+ test[:verify][:check_files].each {|item|
+ exists = File.exist?( item[:location] )
+ if item[:result]
+ assert( exists, "'#{item[:location]}' exists" )
+ else
+ assert( !exists, "'#{item[:location]}' doesn't exists" )
+ end
+ }
+ end
+ }
+
+ end
+end
\ No newline at end of file