package.rb 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  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 { |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. }
  35. return nil if issues.empty?
  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 { |file|
  52. install(file: "#{path}/#{file}")
  53. }
  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 =~ /^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. else
  144. logger.info "Create backup file of #{dest} -> #{backup_file}."
  145. File.rename(dest.to_s, backup_file)
  146. end
  147. end
  148. if File.file?(entry)
  149. if File.symlink?(dest.to_s)
  150. File.delete(dest.to_s)
  151. end
  152. logger.info "Link file: #{entry} -> #{dest}"
  153. File.symlink(entry.to_s, dest.to_s)
  154. end
  155. end
  156. # migration up
  157. Package::Migration.migrate(package)
  158. end
  159. =begin
  160. install zpm package
  161. package = Package.install(file: '/path/to/package.zpm')
  162. or
  163. package = Package.install(string: zpm_as_string)
  164. returns
  165. package # record of new created packae
  166. =end
  167. def self.install(data)
  168. if data[:file]
  169. json = _read_file(data[:file], true)
  170. package = JSON.parse(json)
  171. elsif data[:string]
  172. package = JSON.parse(data[:string])
  173. end
  174. # package meta data
  175. meta = {
  176. name: package['name'],
  177. version: package['version'],
  178. vendor: package['vendor'],
  179. state: 'uninstalled',
  180. created_by_id: 1,
  181. updated_by_id: 1,
  182. }
  183. # verify if package can get installed
  184. package_db = Package.find_by(name: meta[:name])
  185. if package_db
  186. if !data[:reinstall]
  187. if Gem::Version.new(package_db.version) == Gem::Version.new(meta[:version])
  188. raise "Package '#{meta[:name]}-#{meta[:version]}' already installed!"
  189. end
  190. if Gem::Version.new(package_db.version) > Gem::Version.new(meta[:version])
  191. raise "Newer version (#{package_db.version}) of package '#{meta[:name]}-#{meta[:version]}' already installed!"
  192. end
  193. end
  194. # uninstall files of old package
  195. uninstall(
  196. name: package_db.name,
  197. version: package_db.version,
  198. migration_not_down: true,
  199. reinstall: data[:reinstall],
  200. )
  201. end
  202. # store package
  203. if !data[:reinstall]
  204. package_db = Package.create(meta)
  205. Store.add(
  206. object: 'Package',
  207. o_id: package_db.id,
  208. data: package.to_json,
  209. filename: "#{meta[:name]}-#{meta[:version]}.zpm",
  210. preferences: {},
  211. created_by_id: UserInfo.current_user_id || 1,
  212. )
  213. end
  214. # write files
  215. package['files'].each { |file|
  216. permission = file['permission'] || '644'
  217. content = Base64.decode64(file['content'])
  218. _write_file(file['location'], permission, content)
  219. }
  220. # update package state
  221. package_db.state = 'installed'
  222. package_db.save
  223. # up migrations
  224. Package::Migration.migrate(meta[:name])
  225. # prebuild assets
  226. package_db
  227. end
  228. =begin
  229. reinstall package
  230. package = Package.reinstall(package_name)
  231. returns
  232. package # record of new created packae
  233. =end
  234. def self.reinstall(package_name)
  235. package = Package.find_by(name: package_name)
  236. if !package
  237. raise "No such package '#{package_name}'"
  238. end
  239. file = _get_bin(package.name, package.version)
  240. install(string: file, reinstall: true)
  241. package
  242. end
  243. =begin
  244. uninstall package
  245. package = Package.uninstall(name: 'package', version: '0.1.1')
  246. or
  247. package = Package.uninstall(string: zpm_as_string)
  248. returns
  249. package # record of new created packae
  250. =end
  251. def self.uninstall(data)
  252. if data[:string]
  253. package = JSON.parse(data[:string])
  254. else
  255. json_file = _get_bin(data[:name], data[:version])
  256. package = JSON.parse(json_file)
  257. end
  258. # down migrations
  259. if !data[:migration_not_down]
  260. Package::Migration.migrate(package['name'], 'reverse')
  261. end
  262. package['files'].each { |file|
  263. permission = file['permission'] || '644'
  264. content = Base64.decode64(file['content'])
  265. _delete_file(file['location'], permission, content)
  266. }
  267. # delete package
  268. if !data[:reinstall]
  269. record = Package.find_by(
  270. name: package['name'],
  271. version: package['version'],
  272. )
  273. record.destroy
  274. end
  275. record
  276. end
  277. =begin
  278. execute all pending package migrations at once
  279. Package.migration_execute
  280. =end
  281. def self.migration_execute
  282. Package.all.each { |package|
  283. json_file = Package._get_bin(package.name, package.version)
  284. package = JSON.parse(json_file)
  285. Package::Migration.migrate(package['name'])
  286. }
  287. end
  288. def self._get_bin(name, version)
  289. package = Package.find_by(
  290. name: name,
  291. version: version,
  292. )
  293. if !package
  294. raise "No such package '#{name}' version '#{version}'"
  295. end
  296. list = Store.list(
  297. object: 'Package',
  298. o_id: package.id,
  299. )
  300. # find file
  301. if !list || !list.first
  302. raise "No such file in storage list #{name} #{version}"
  303. end
  304. if !list.first.content
  305. raise "No such file in storage #{name} #{version}"
  306. end
  307. list.first.content
  308. end
  309. def self._read_file(file, fullpath = false)
  310. location = if fullpath == false
  311. @@root + '/' + file
  312. elsif fullpath == true
  313. file
  314. else
  315. fullpath + '/' + file
  316. end
  317. begin
  318. data = File.open(location, 'rb')
  319. contents = data.read
  320. rescue => e
  321. raise 'ERROR: ' + e.inspect
  322. end
  323. contents
  324. end
  325. def self._write_file(file, permission, data)
  326. location = "#{@@root}/#{file}"
  327. # rename existing file if not already the same file
  328. if File.exist?(location)
  329. content_fs = _read_file(file)
  330. if content_fs == data
  331. logger.debug "NOTICE: file '#{location}' already exists, skip install"
  332. return true
  333. end
  334. backup_location = location + '.save'
  335. logger.info "NOTICE: backup old file '#{location}' to #{backup_location}"
  336. File.rename(location, backup_location)
  337. end
  338. # check if directories need to be created
  339. directories = location.split '/'
  340. (0..(directories.length - 2) ).each { |position|
  341. tmp_path = ''
  342. (1..position).each { |count|
  343. tmp_path = "#{tmp_path}/#{directories[count]}"
  344. }
  345. next if tmp_path == ''
  346. next if File.exist?(tmp_path)
  347. Dir.mkdir(tmp_path, 0o755)
  348. }
  349. # install file
  350. begin
  351. logger.info "NOTICE: install '#{location}' (#{permission})"
  352. file = File.new(location, 'wb')
  353. file.write(data)
  354. file.close
  355. File.chmod(permission.to_s.to_i(8), location)
  356. rescue => e
  357. raise 'ERROR: ' + e.inspect
  358. end
  359. true
  360. end
  361. def self._delete_file(file, _permission, _data)
  362. location = "#{@@root}/#{file}"
  363. # install file
  364. logger.info "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. logger.info "NOTICE: restore old file '#{backup_location}' to #{location}"
  372. File.rename(backup_location, location)
  373. end
  374. true
  375. end
  376. class Migration < ApplicationModel
  377. @@root = Rails.root.to_s # rubocop:disable Style/ClassVars
  378. def self.migrate(package, direction = 'normal')
  379. location = "#{@@root}/db/addon/#{package.underscore}"
  380. return true if !File.exist?(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.find_by(name: package.underscore, version: version)
  409. next if !done
  410. logger.info "NOTICE: down package migration '#{migration}'"
  411. load "#{location}/#{migration}"
  412. classname = name.camelcase
  413. Kernel.const_get(classname).down
  414. record = Package::Migration.find_by(name: package.underscore, version: version)
  415. if record
  416. record.destroy
  417. end
  418. # up
  419. else
  420. done = Package::Migration.find_by(name: package.underscore, version: version)
  421. next if done
  422. logger.info "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. }
  429. end
  430. end
  431. end