mirror of
				https://github.com/redmine/redmine.git
				synced 2025-10-31 18:36:07 +01:00 
			
		
		
		
	
		
			
	
	
		
			214 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
		
		
			
		
	
	
			214 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
|  | # frozen_string_literal: true | ||
|  | 
 | ||
|  | # Redmine - project management software | ||
|  | # Copyright (C) 2006-2023  Jean-Philippe Lang | ||
|  | # | ||
|  | # This program is free software; you can redistribute it and/or | ||
|  | # modify it under the terms of the GNU General Public License | ||
|  | # as published by the Free Software Foundation; either version 2 | ||
|  | # of the License, or (at your option) any later version. | ||
|  | # | ||
|  | # This program is distributed in the hope that it will be useful, | ||
|  | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||
|  | # GNU General Public License for more details. | ||
|  | # | ||
|  | # You should have received a copy of the GNU General Public License | ||
|  | # along with this program; if not, write to the Free Software | ||
|  | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. | ||
|  | 
 | ||
|  | module Redmine | ||
|  |   class AssetPath | ||
|  | 
 | ||
|  |     attr_reader :paths, :prefix, :version | ||
|  | 
 | ||
|  |     def initialize(base_dir, paths, prefix=nil) | ||
|  |       @base_dir = base_dir | ||
|  |       @paths  = paths | ||
|  |       @prefix = prefix | ||
|  |       @transition = Transition.new(src: Set.new, dest: Set.new) | ||
|  |       @version = Rails.application.config.assets.version | ||
|  |     end | ||
|  | 
 | ||
|  |     def update(transition_map:, assets:) | ||
|  |       each_file do |file, intermediate_path, logical_path| | ||
|  |         @transition.add_src  intermediate_path, logical_path | ||
|  |         @transition.add_dest intermediate_path, logical_path | ||
|  |         asset = file.extname == '.css' ? Redmine::Asset.new(file,   logical_path: logical_path, version: version, transition_map: transition_map) | ||
|  |                   : Propshaft::Asset.new(file, logical_path: logical_path, version: version) | ||
|  |         assets[asset.logical_path.to_s] ||= asset | ||
|  |       end | ||
|  |       @transition.update(transition_map) | ||
|  |       nil | ||
|  |     end | ||
|  | 
 | ||
|  |     def each_file | ||
|  |       paths.each do |path| | ||
|  |         without_dotfiles(all_files_from_tree(path)).each do |file| | ||
|  |           relative_path = file.relative_path_from(path).to_s | ||
|  |           logical_path  = prefix ? File.join(prefix, relative_path) : relative_path | ||
|  |           intermediate_path = Pathname.new("/#{prefix}").join(file.relative_path_from(@base_dir)) | ||
|  |           yield file, intermediate_path, logical_path | ||
|  |         end | ||
|  |       end | ||
|  |     end | ||
|  | 
 | ||
|  |     private | ||
|  | 
 | ||
|  |     Transition = Struct.new(:src, :dest, keyword_init: true) do | ||
|  | 
 | ||
|  |       def add_src(file, logical_path) | ||
|  |         src.add  path_pair(file, logical_path) if file.extname == '.css' | ||
|  |       end | ||
|  | 
 | ||
|  |       def add_dest(file, logical_path) | ||
|  |         return if file.extname == '.js' || file.extname == '.map' | ||
|  |         # No parent-child directories are needed in dest. | ||
|  |         dirname = file.dirname | ||
|  |         if child = dest.find{|d| child_path? dirname, d[0]} | ||
|  |           dest.delete child | ||
|  |           dest.add path_pair(file, logical_path) | ||
|  |         elsif !dest.any?{|d| parent_path? dirname, d[0]} | ||
|  |           dest.add path_pair(file, logical_path) | ||
|  |         end | ||
|  |       end | ||
|  | 
 | ||
|  |       def path_pair(file, logical_path) | ||
|  |         [file.dirname, Pathname.new("/#{logical_path}").dirname] | ||
|  |       end | ||
|  | 
 | ||
|  |       def parent_path?(path, other) | ||
|  |         return nil if other == path | ||
|  |         path.ascend.any?{|v| v == other} | ||
|  |       end | ||
|  | 
 | ||
|  |       def child_path?(path, other) | ||
|  |         return nil if path == other | ||
|  |         other.ascend.any?{|v| v == path} | ||
|  |       end | ||
|  | 
 | ||
|  |       def update(transition_map) | ||
|  |         product = src.to_a.product(dest.to_a).select{|t| t[0] != t[1]} | ||
|  |         maps = product.map do |t| | ||
|  |           AssetPathMap.new(src: t[0][0], dest: t[1][0], logical_src: t[0][1], logical_dest: t[1][1]) | ||
|  |         end | ||
|  |         maps.each do |m| | ||
|  |           if m.before != m.after | ||
|  |             transition_map[m.dirname] ||= {} | ||
|  |             transition_map[m.dirname][m.before] = m.after | ||
|  |           end | ||
|  |         end | ||
|  |       end | ||
|  |     end | ||
|  | 
 | ||
|  |     AssetPathMap = Struct.new(:src, :dest, :logical_src, :logical_dest, keyword_init: true) do | ||
|  | 
 | ||
|  |       def dirname | ||
|  |         key = logical_src.to_s.sub('/', '') | ||
|  |         key == '' ? '.' : key | ||
|  |       end | ||
|  | 
 | ||
|  |       def before | ||
|  |         dest.relative_path_from(src).to_s | ||
|  |       end | ||
|  | 
 | ||
|  |       def after | ||
|  |         logical_dest.relative_path_from(logical_src).to_s | ||
|  |       end | ||
|  |     end | ||
|  | 
 | ||
|  |     def without_dotfiles(files) | ||
|  |       files.reject { |file| file.basename.to_s.starts_with?(".") } | ||
|  |     end | ||
|  | 
 | ||
|  |     def all_files_from_tree(path) | ||
|  |       path.children.flat_map { |child| child.directory? ? all_files_from_tree(child) : child } | ||
|  |     end | ||
|  |   end | ||
|  | 
 | ||
|  |   class AssetLoadPath < Propshaft::LoadPath | ||
|  | 
 | ||
|  |     attr_reader :extension_paths, :default_asset_path, :transition_map | ||
|  | 
 | ||
|  |     def initialize(config) | ||
|  |       @extension_paths    = config.redmine_extension_paths | ||
|  |       @default_asset_path = config.redmine_default_asset_path | ||
|  |       super(config.paths, version: config.version) | ||
|  |     end | ||
|  | 
 | ||
|  |     def asset_files | ||
|  |       Enumerator.new do |y| | ||
|  |         Rails.logger.info all_paths | ||
|  |         all_paths.each do |path| | ||
|  |           next unless path.exist? | ||
|  |           without_dotfiles(all_files_from_tree(path)).each do |file| | ||
|  |             y << file | ||
|  |           end | ||
|  |         end | ||
|  |       end | ||
|  |     end | ||
|  | 
 | ||
|  |     def assets_by_path | ||
|  |       merge_required = @cached_assets_by_path == nil | ||
|  |       super | ||
|  |       if merge_required | ||
|  |         @transition_map = {} | ||
|  |         default_asset_path.update(assets: @cached_assets_by_path, transition_map: transition_map) | ||
|  |         extension_paths.each do |asset_path| | ||
|  |           # Support link from extension assets to assets in the application | ||
|  |           default_asset_path.each_file do |file, intermediate_path, logical_path| | ||
|  |             asset_path.instance_eval { @transition.add_dest intermediate_path, logical_path } | ||
|  |           end | ||
|  |           asset_path.update(assets: @cached_assets_by_path, transition_map: transition_map) | ||
|  |         end | ||
|  |       end | ||
|  |       @cached_assets_by_path | ||
|  |     end | ||
|  | 
 | ||
|  |     def cache_sweeper | ||
|  |       @cache_sweeper ||= begin | ||
|  |                            exts_to_watch  = Mime::EXTENSION_LOOKUP.map(&:first) | ||
|  |                            files_to_watch = Array(all_paths).collect { |dir| [ dir.to_s, exts_to_watch ] }.to_h | ||
|  |                            Rails.application.config.file_watcher.new([], files_to_watch) do | ||
|  |                              clear_cache | ||
|  |                            end | ||
|  |                          end | ||
|  |     end | ||
|  | 
 | ||
|  |     def all_paths | ||
|  |       [paths, default_asset_path.paths, extension_paths.map{|path| path.paths}].flatten.compact | ||
|  |     end | ||
|  | 
 | ||
|  |     def clear_cache | ||
|  |       @transition_map = nil | ||
|  |       super | ||
|  |     end | ||
|  |   end | ||
|  | 
 | ||
|  |   class Asset < Propshaft::Asset | ||
|  |     def initialize(file, logical_path:, version:, transition_map:) | ||
|  |       @transition_map = transition_map | ||
|  |       super(file, logical_path: logical_path, version: version) | ||
|  |     end | ||
|  | 
 | ||
|  |     def content | ||
|  |       if conversion = @transition_map[logical_path.dirname.to_s] | ||
|  |         convert_path super, conversion | ||
|  |       else | ||
|  |         super | ||
|  |       end | ||
|  |     end | ||
|  | 
 | ||
|  |     ASSET_URL_PATTERN = /(url\(\s*["']?([^"'\s)]+)\s*["']?\s*\))/ | ||
|  | 
 | ||
|  |     def convert_path(input, conversion) | ||
|  |       input.gsub(ASSET_URL_PATTERN) do |matched| | ||
|  |         conversion.each do |key, val| | ||
|  |           matched.sub!(key, val) | ||
|  |         end | ||
|  |         matched | ||
|  |       end | ||
|  |     end | ||
|  |   end | ||
|  | end |