package.rb 13 KB

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