From e4dd4a0d5d7e6e550a0f68bbfa6014457bc0520a Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 26 Dec 2012 23:02:16 +0100 Subject: [PATCH] Init version of package manager backend. --- app/models/package.rb | 296 ++++++++++++++++++++ db/migrate/20121225162100_create_package.rb | 23 ++ test/test_helper.rb | 3 + test/unit/package_test.rb | 209 ++++++++++++++ 4 files changed, 531 insertions(+) create mode 100644 app/models/package.rb create mode 100644 db/migrate/20121225162100_create_package.rb create mode 100644 test/unit/package_test.rb 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