package.rb 11 KB

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