2024-01-25 05:38:33 +00:00
|
|
|
# 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
|
2024-01-26 02:39:47 +00:00
|
|
|
asset = if file.extname == '.css'
|
|
|
|
|
Redmine::Asset.new(file, logical_path: logical_path, version: version, transition_map: transition_map)
|
|
|
|
|
else
|
|
|
|
|
Propshaft::Asset.new(file, logical_path: logical_path, version: version)
|
|
|
|
|
end
|
2024-01-25 05:38:33 +00:00
|
|
|
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'
|
2024-01-26 02:39:47 +00:00
|
|
|
|
2024-01-25 05:38:33 +00:00
|
|
|
# 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)
|
2024-01-26 08:19:24 +00:00
|
|
|
return false if other == path
|
2024-01-26 02:39:47 +00:00
|
|
|
|
2024-01-26 08:02:50 +00:00
|
|
|
path.ascend.any?(other)
|
2024-01-25 05:38:33 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def child_path?(path, other)
|
2024-01-26 08:19:24 +00:00
|
|
|
return false if path == other
|
2024-01-26 02:39:47 +00:00
|
|
|
|
2024-01-26 08:02:50 +00:00
|
|
|
other.ascend.any?(path)
|
2024-01-25 05:38:33 +00:00
|
|
|
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?
|
2024-01-26 02:39:47 +00:00
|
|
|
|
2024-01-25 05:38:33 +00:00
|
|
|
without_dotfiles(all_files_from_tree(path)).each do |file|
|
|
|
|
|
y << file
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def assets_by_path
|
2024-01-26 02:39:47 +00:00
|
|
|
merge_required = @cached_assets_by_path.nil?
|
2024-01-25 05:38:33 +00:00
|
|
|
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
|
2024-01-26 02:39:47 +00:00
|
|
|
exts_to_watch = Mime::EXTENSION_LOOKUP.map(&:first)
|
2024-01-26 08:29:59 +00:00
|
|
|
files_to_watch = Array(all_paths).to_h { |dir| [dir.to_s, exts_to_watch] }
|
2024-01-26 02:39:47 +00:00
|
|
|
Rails.application.config.file_watcher.new([], files_to_watch) do
|
|
|
|
|
clear_cache
|
|
|
|
|
end
|
|
|
|
|
end
|
2024-01-25 05:38:33 +00:00
|
|
|
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
|