You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

asset_path.rb 6.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. # frozen_string_literal: true
  2. # Redmine - project management software
  3. # Copyright (C) 2006-2023 Jean-Philippe Lang
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License
  7. # as published by the Free Software Foundation; either version 2
  8. # of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. module Redmine
  19. class AssetPath
  20. attr_reader :paths, :prefix, :version
  21. def initialize(base_dir, paths, prefix=nil)
  22. @base_dir = base_dir
  23. @paths = paths
  24. @prefix = prefix
  25. @transition = Transition.new(src: Set.new, dest: Set.new)
  26. @version = Rails.application.config.assets.version
  27. end
  28. def update(transition_map:, assets:)
  29. each_file do |file, intermediate_path, logical_path|
  30. @transition.add_src intermediate_path, logical_path
  31. @transition.add_dest intermediate_path, logical_path
  32. asset = file.extname == '.css' ? Redmine::Asset.new(file, logical_path: logical_path, version: version, transition_map: transition_map)
  33. : Propshaft::Asset.new(file, logical_path: logical_path, version: version)
  34. assets[asset.logical_path.to_s] ||= asset
  35. end
  36. @transition.update(transition_map)
  37. nil
  38. end
  39. def each_file
  40. paths.each do |path|
  41. without_dotfiles(all_files_from_tree(path)).each do |file|
  42. relative_path = file.relative_path_from(path).to_s
  43. logical_path = prefix ? File.join(prefix, relative_path) : relative_path
  44. intermediate_path = Pathname.new("/#{prefix}").join(file.relative_path_from(@base_dir))
  45. yield file, intermediate_path, logical_path
  46. end
  47. end
  48. end
  49. private
  50. Transition = Struct.new(:src, :dest, keyword_init: true) do
  51. def add_src(file, logical_path)
  52. src.add path_pair(file, logical_path) if file.extname == '.css'
  53. end
  54. def add_dest(file, logical_path)
  55. return if file.extname == '.js' || file.extname == '.map'
  56. # No parent-child directories are needed in dest.
  57. dirname = file.dirname
  58. if child = dest.find{|d| child_path? dirname, d[0]}
  59. dest.delete child
  60. dest.add path_pair(file, logical_path)
  61. elsif !dest.any?{|d| parent_path? dirname, d[0]}
  62. dest.add path_pair(file, logical_path)
  63. end
  64. end
  65. def path_pair(file, logical_path)
  66. [file.dirname, Pathname.new("/#{logical_path}").dirname]
  67. end
  68. def parent_path?(path, other)
  69. return nil if other == path
  70. path.ascend.any?{|v| v == other}
  71. end
  72. def child_path?(path, other)
  73. return nil if path == other
  74. other.ascend.any?{|v| v == path}
  75. end
  76. def update(transition_map)
  77. product = src.to_a.product(dest.to_a).select{|t| t[0] != t[1]}
  78. maps = product.map do |t|
  79. AssetPathMap.new(src: t[0][0], dest: t[1][0], logical_src: t[0][1], logical_dest: t[1][1])
  80. end
  81. maps.each do |m|
  82. if m.before != m.after
  83. transition_map[m.dirname] ||= {}
  84. transition_map[m.dirname][m.before] = m.after
  85. end
  86. end
  87. end
  88. end
  89. AssetPathMap = Struct.new(:src, :dest, :logical_src, :logical_dest, keyword_init: true) do
  90. def dirname
  91. key = logical_src.to_s.sub('/', '')
  92. key == '' ? '.' : key
  93. end
  94. def before
  95. dest.relative_path_from(src).to_s
  96. end
  97. def after
  98. logical_dest.relative_path_from(logical_src).to_s
  99. end
  100. end
  101. def without_dotfiles(files)
  102. files.reject { |file| file.basename.to_s.starts_with?(".") }
  103. end
  104. def all_files_from_tree(path)
  105. path.children.flat_map { |child| child.directory? ? all_files_from_tree(child) : child }
  106. end
  107. end
  108. class AssetLoadPath < Propshaft::LoadPath
  109. attr_reader :extension_paths, :default_asset_path, :transition_map
  110. def initialize(config)
  111. @extension_paths = config.redmine_extension_paths
  112. @default_asset_path = config.redmine_default_asset_path
  113. super(config.paths, version: config.version)
  114. end
  115. def asset_files
  116. Enumerator.new do |y|
  117. Rails.logger.info all_paths
  118. all_paths.each do |path|
  119. next unless path.exist?
  120. without_dotfiles(all_files_from_tree(path)).each do |file|
  121. y << file
  122. end
  123. end
  124. end
  125. end
  126. def assets_by_path
  127. merge_required = @cached_assets_by_path == nil
  128. super
  129. if merge_required
  130. @transition_map = {}
  131. default_asset_path.update(assets: @cached_assets_by_path, transition_map: transition_map)
  132. extension_paths.each do |asset_path|
  133. # Support link from extension assets to assets in the application
  134. default_asset_path.each_file do |file, intermediate_path, logical_path|
  135. asset_path.instance_eval { @transition.add_dest intermediate_path, logical_path }
  136. end
  137. asset_path.update(assets: @cached_assets_by_path, transition_map: transition_map)
  138. end
  139. end
  140. @cached_assets_by_path
  141. end
  142. def cache_sweeper
  143. @cache_sweeper ||= begin
  144. exts_to_watch = Mime::EXTENSION_LOOKUP.map(&:first)
  145. files_to_watch = Array(all_paths).collect { |dir| [ dir.to_s, exts_to_watch ] }.to_h
  146. Rails.application.config.file_watcher.new([], files_to_watch) do
  147. clear_cache
  148. end
  149. end
  150. end
  151. def all_paths
  152. [paths, default_asset_path.paths, extension_paths.map{|path| path.paths}].flatten.compact
  153. end
  154. def clear_cache
  155. @transition_map = nil
  156. super
  157. end
  158. end
  159. class Asset < Propshaft::Asset
  160. def initialize(file, logical_path:, version:, transition_map:)
  161. @transition_map = transition_map
  162. super(file, logical_path: logical_path, version: version)
  163. end
  164. def content
  165. if conversion = @transition_map[logical_path.dirname.to_s]
  166. convert_path super, conversion
  167. else
  168. super
  169. end
  170. end
  171. ASSET_URL_PATTERN = /(url\(\s*["']?([^"'\s)]+)\s*["']?\s*\))/
  172. def convert_path(input, conversion)
  173. input.gsub(ASSET_URL_PATTERN) do |matched|
  174. conversion.each do |key, val|
  175. matched.sub!(key, val)
  176. end
  177. matched
  178. end
  179. end
  180. end
  181. end