Merge cc0ff4650b60f37766666373019d7b100b46bfa9 into 6afe0e29a7818313e8a3295a13bb8ed92d3d354c

This commit is contained in:
Alexandre Flament 2025-03-15 07:50:31 +01:00 committed by GitHub
commit 7fd835785b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 263 additions and 56 deletions

View File

@ -16,8 +16,8 @@
var map_boundingbox = JSON.parse(this.dataset.mapBoundingbox); var map_boundingbox = JSON.parse(this.dataset.mapBoundingbox);
var map_geojson = JSON.parse(this.dataset.mapGeojson); var map_geojson = JSON.parse(this.dataset.mapGeojson);
searxng.loadStyle('css/leaflet.css'); searxng.loadStyle('css/leaflet.SEARXNG_HASH.css');
searxng.loadScript('js/leaflet.js', function () { searxng.loadScript('js/leaflet.SEARXNG_HASH.js', function () {
var map_bounds = null; var map_bounds = null;
if (map_boundingbox) { if (map_boundingbox) {
var southWest = L.latLng(map_boundingbox[0], map_boundingbox[2]); var southWest = L.latLng(map_boundingbox[0], map_boundingbox[2]);

View File

@ -6,7 +6,7 @@
text-align: center; text-align: center;
.title { .title {
background: url("../img/searxng.png") no-repeat; background: url("../img/searxng.SEARXNG_HASH.png") no-repeat;
min-height: 4rem; min-height: 4rem;
margin: 4rem auto; margin: 4rem auto;
background-position: center; background-position: center;

View File

@ -2,12 +2,16 @@
* CONFIG: https://vite.dev/config/ * CONFIG: https://vite.dev/config/
*/ */
import { resolve } from "node:path"; import { resolve, relative } from "node:path";
import { Buffer } from 'buffer';
import path from 'path';
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import stylelint from "vite-plugin-stylelint"; import stylelint from "vite-plugin-stylelint";
import { viteStaticCopy } from "vite-plugin-static-copy"; import { viteStaticCopy } from "vite-plugin-static-copy";
import { plg_svg2png } from "./tools/plg.js"; import { plg_svg2png } from "./tools/plg.js";
import { plg_svg2svg } from "./tools/plg.js"; import { plg_svg2svg } from "./tools/plg.js";
import fs from 'node:fs/promises';
import crypto from 'node:crypto';
const ROOT = "../.."; // root of the git reposetory const ROOT = "../.."; // root of the git reposetory
@ -40,6 +44,155 @@ const svg2svg_favicon_opts = {
] ]
}; };
function AddSearxNGHashes(options = {}) {
const {
fileName = "hashes.json",
exclude = [],
include_without_hashes = []
} = options;
let outDir = null;
// Helper: recursively get all files (not directories) within `dir`.
async function getAllFiles(dir) {
let entries = await fs.readdir(dir, { withFileTypes: true });
let files = [];
for (const entry of entries) {
const fullPath = resolve(dir, entry.name);
if (entry.isDirectory()) {
files = files.concat(await getAllFiles(fullPath));
} else {
files.push(fullPath);
}
}
// Separate out `.map` files so they end up last
const mapFiles = files.filter((file) => file.endsWith(".map"));
const otherFiles = files.filter((file) => !file.endsWith(".map"));
return [...otherFiles, ...mapFiles];
}
function replacePathsInBuffer(body, mapping) {
// Convert the Buffer to a string (assuming UTF-8)
let content = body.toString("utf-8");
// Perform replacements
for (const logicalPath of Object.keys(mapping)) {
const hashedPath = mapping[logicalPath];
content = content.replaceAll(logicalPath, hashedPath);
}
// Convert the modified string back to a Buffer
return Buffer.from(content, "utf-8");
}
return {
name: "recursive-hash-manifest-plugin",
apply: "build",
// Capture the final "outDir" from the resolved Vite config
configResolved(config) {
outDir = config.build.outDir;
},
// "closeBundle" is called after everything (including other async tasks) is done writing
async closeBundle() {
// Check if the outDir is set (from configResolved)
if (outDir === null) {
return
}
// Get a list of every file in the output directory
let allFiles = await getAllFiles(outDir);
// Optionally exclude certain files
const exclusionSet = new Set([...exclude, fileName]);
allFiles = allFiles.filter((filePath) => {
const relPath = relative(outDir, filePath);
return !exclusionSet.has(relPath);
});
// Compute a hash for each file
const assets = {};
const var_mapping = {}
const hash_override = {}
for (const filePath of allFiles) {
const relPath = relative(outDir, filePath);
// Get the shortHash
let shortHash;
if (include_without_hashes.includes(relative(outDir, filePath))) {
shortHash = "";
} else if (Object.prototype.hasOwnProperty.call(hash_override, filePath)) {
shortHash = hash_override[filePath];
} else {
const fileBuf = await fs.readFile(filePath);
const hashSum = crypto.createHash("sha256").update(fileBuf).digest("hex");
shortHash = "." + hashSum.slice(0, 8);
hash_override[filePath + ".map"] = shortHash;
}
// Prepare to build a new file path
const dirName = path.dirname(filePath);
let newFilePath;
let varPath = null;
// Special handling for *.js.map
if (filePath.endsWith(".js.map")) {
const baseName = path.basename(filePath, ".js.map");
newFilePath = path.join(dirName, `${baseName}${shortHash}.js.map`);
}
// Special handling for *.css.map
else if (filePath.endsWith(".css.map")) {
const baseName = path.basename(filePath, ".css.map");
newFilePath = path.join(dirName, `${baseName}${shortHash}.css.map`);
}
// Otherwise, rename as usual
else {
const extName = path.extname(filePath);
const baseName = path.basename(filePath, extName);
newFilePath = path.join(dirName, `${baseName}${shortHash}${extName}`);
//
varPath = `${baseName}.SEARXNG_HASH${extName}`;
var_mapping[varPath] = `${baseName}${shortHash}${extName}`;
if (filePath.endsWith(".js")) {
var_mapping[`//# sourceMappingURL=${baseName}${extName}.map`] = `//# sourceMappingURL=${baseName}${shortHash}${extName}.map`;
}
}
// New relative path
const newRelPath = relative(outDir, newFilePath);
assets[relPath] = newRelPath;
}
// Step 2: Once the manifest is all set, read back files that might reference others
// and replace placeholders with hashed paths.
for (const filePath of allFiles) {
const extName = path.extname(filePath);
if (![".css", ".js", ".html"].includes(extName)) {
continue;
}
const originalBuf = await fs.readFile(filePath);
const replacedBuf = replacePathsInBuffer(originalBuf, var_mapping);
await fs.writeFile(filePath, replacedBuf);
}
// Step 3: rename the original files to their hashed filenames
for (const filePath of allFiles) {
const relPath = path.relative(outDir, filePath);
const newRelPath = assets[relPath];
const newFilePath = path.join(outDir, newRelPath);
await fs.rename(filePath, newFilePath);
}
// Write out `assets.json`
const assetsPath = resolve(outDir, fileName);
await fs.writeFile(assetsPath, JSON.stringify(assets, null, 2), "utf-8");
},
};
}
export default defineConfig({ export default defineConfig({
@ -180,6 +333,22 @@ export default defineConfig({
svg2svg_opts svg2svg_opts
), ),
// -- create assets.json and add hashes to files
AddSearxNGHashes({
fileName: "assets.json",
exclude: [
".gitattributes",
"manifest.json"
],
include_without_hashes: [
"css/images/layers-2x.png",
"css/images/layers.png",
"css/images/marker-icon-2x.png",
"css/images/marker-icon.png",
"css/images/marker-shadow.png",
]
}),
] // end: plugins ] // end: plugins
}); });

View File

@ -117,8 +117,6 @@ redis:
ui: ui:
# Custom static path - leave it blank if you didn't change # Custom static path - leave it blank if you didn't change
static_path: "" static_path: ""
# Is overwritten by ${SEARXNG_STATIC_USE_HASH}.
static_use_hash: false
# Custom templates path - leave it blank if you didn't change # Custom templates path - leave it blank if you didn't change
templates_path: "" templates_path: ""
# query_in_title: When true, the result page's titles contains the query # query_in_title: When true, the result page's titles contains the query

View File

@ -190,7 +190,6 @@ SCHEMA = {
}, },
'ui': { 'ui': {
'static_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'static')), 'static_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'static')),
'static_use_hash': SettingsValue(bool, False, 'SEARXNG_STATIC_USE_HASH'),
'templates_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'templates')), 'templates_path': SettingsDirectoryValue(str, os.path.join(searx_dir, 'templates')),
'default_theme': SettingsValue(str, 'simple'), 'default_theme': SettingsValue(str, 'simple'),
'default_locale': SettingsValue(str, ''), 'default_locale': SettingsValue(str, ''),

View File

@ -0,0 +1,25 @@
{
"css/images/layers-2x.png": "css/images/layers-2x.png",
"css/images/layers.png": "css/images/layers.png",
"css/images/marker-icon-2x.png": "css/images/marker-icon-2x.png",
"css/images/marker-icon.png": "css/images/marker-icon.png",
"css/images/marker-shadow.png": "css/images/marker-shadow.png",
"css/leaflet.css": "css/leaflet.a7837102.css",
"css/rss.min.css": "css/rss.min.fe304faa.css",
"css/searxng-rtl.min.css": "css/searxng-rtl.min.726accde.css",
"css/searxng.min.css": "css/searxng.min.21f0e928.css",
"img/empty_favicon.svg": "img/empty_favicon.73e4f03b.svg",
"img/favicon.png": "img/favicon.1bf18897.png",
"img/favicon.svg": "img/favicon.c852f912.svg",
"img/img_load_error.svg": "img/img_load_error.d0fe32fb.svg",
"img/searxng.png": "img/searxng.d79d0c2c.png",
"img/searxng.svg": "img/searxng.25892637.svg",
"img/select-dark.svg": "img/select-dark.b971594e.svg",
"img/select-light.svg": "img/select-light.5f366b16.svg",
"js/leaflet.js": "js/leaflet.db49d009.js",
"js/searxng.head.min.js": "js/searxng.head.min.27558b92.js",
"js/searxng.min.js": "js/searxng.min.c2c12312.js",
"js/leaflet.js.map": "js/leaflet.db49d009.js.map",
"js/searxng.head.min.js.map": "js/searxng.head.min.27558b92.js.map",
"js/searxng.min.js.map": "js/searxng.min.c2c12312.js.map"
}

View File

Before

Width:  |  Height:  |  Size: 966 B

After

Width:  |  Height:  |  Size: 966 B

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 726 B

After

Width:  |  Height:  |  Size: 726 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 120 B

After

Width:  |  Height:  |  Size: 120 B

View File

Before

Width:  |  Height:  |  Size: 108 B

After

Width:  |  Height:  |  Size: 108 B

View File

@ -1,2 +1,2 @@
(function(n,t){var r=t.currentScript||function(){var s=t.getElementsByTagName("script");return s[s.length-1]}();n.searxng={settings:JSON.parse(atob(r.getAttribute("client_settings")))};var e=t.getElementsByTagName("html")[0];e.classList.remove("no-js"),e.classList.add("js")})(window,document); (function(n,t){var r=t.currentScript||function(){var s=t.getElementsByTagName("script");return s[s.length-1]}();n.searxng={settings:JSON.parse(atob(r.getAttribute("client_settings")))};var e=t.getElementsByTagName("html")[0];e.classList.remove("no-js"),e.classList.add("js")})(window,document);
//# sourceMappingURL=searxng.head.min.js.map //# sourceMappingURL=searxng.head.min.27558b92.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -16,6 +16,7 @@ import base64
from timeit import default_timer from timeit import default_timer
from html import escape from html import escape
from io import StringIO from io import StringIO
from pathlib import Path
import typing import typing
import urllib import urllib
@ -63,7 +64,7 @@ from searx.botdetection import link_token
from searx.data import ENGINE_DESCRIPTIONS from searx.data import ENGINE_DESCRIPTIONS
from searx.result_types import Answer from searx.result_types import Answer
from searx.settings_defaults import OUTPUT_FORMATS from searx.settings_defaults import OUTPUT_FORMATS
from searx.settings_loader import DEFAULT_SETTINGS_FILE from searx.settings_loader import DEFAULT_SETTINGS_FILE, searx_dir
from searx.exceptions import SearxParameterException from searx.exceptions import SearxParameterException
from searx.engines import ( from searx.engines import (
DEFAULT_CATEGORY, DEFAULT_CATEGORY,
@ -244,24 +245,21 @@ def get_result_template(theme_name: str, template_name: str):
def custom_url_for(endpoint: str, **values): def custom_url_for(endpoint: str, **values):
suffix = ""
if endpoint == 'static' and values.get('filename'): if endpoint == 'static' and values.get('filename'):
file_hash = static_files.get(values['filename']) actual_filename = static_files.get(values['filename'])
if not file_hash: if not actual_filename:
# try file in the current theme # try file in the current theme
theme_name = sxng_request.preferences.get_value('theme') theme_name = sxng_request.preferences.get_value('theme')
filename_with_theme = "themes/{}/{}".format(theme_name, values['filename']) logical_filename = "themes/{}/{}".format(theme_name, values['filename'])
file_hash = static_files.get(filename_with_theme) actual_filename = static_files.get(logical_filename)
if file_hash: if actual_filename:
values['filename'] = filename_with_theme values['filename'] = actual_filename
if get_setting('ui.static_use_hash') and file_hash:
suffix = "?" + file_hash
if endpoint == 'info' and 'locale' not in values: if endpoint == 'info' and 'locale' not in values:
locale = sxng_request.preferences.get_value('locale') locale = sxng_request.preferences.get_value('locale')
if infopage.INFO_PAGES.get_page(values['pagename'], locale) is None: if infopage.INFO_PAGES.get_page(values['pagename'], locale) is None:
locale = infopage.INFO_PAGES.locale_default locale = infopage.INFO_PAGES.locale_default
values['locale'] = locale values['locale'] = locale
return url_for(endpoint, **values) + suffix return url_for(endpoint, **values)
def morty_proxify(url: str): def morty_proxify(url: str):
@ -1250,9 +1248,11 @@ def opensearch():
@app.route('/favicon.ico') @app.route('/favicon.ico')
def favicon(): def favicon():
theme = sxng_request.preferences.get_value("theme") theme = sxng_request.preferences.get_value("theme")
logical_file_name = 'themes/' + theme + '/img/favicon.png'
actual_file_name = static_files.get(logical_file_name, logical_file_name)
return send_from_directory( return send_from_directory(
os.path.join(app.root_path, settings['ui']['static_path'], 'themes', theme, 'img'), # type: ignore os.path.join(app.root_path, settings['ui']['static_path']), # type: ignore
'favicon.png', actual_file_name,
mimetype='image/vnd.microsoft.icon', mimetype='image/vnd.microsoft.icon',
) )
@ -1365,7 +1365,10 @@ def run():
port=settings['server']['port'], port=settings['server']['port'],
host=settings['server']['bind_address'], host=settings['server']['bind_address'],
threaded=True, threaded=True,
extra_files=[DEFAULT_SETTINGS_FILE], extra_files=[
DEFAULT_SETTINGS_FILE,
Path(searx_dir) / "static/themes/simple/assets.json",
],
) )

View File

@ -178,30 +178,45 @@ def get_themes(templates_path):
return os.listdir(templates_path) return os.listdir(templates_path)
def get_hash_for_file(file: pathlib.Path) -> str: def get_static_files_legacy(static_path: str, path: pathlib.Path):
m = hashlib.sha1() result: list[str] = []
with file.open('rb') as f: for file in path.iterdir():
m.update(f.read()) if file.name.startswith('.'):
return m.hexdigest() # ignore hidden file
continue
if file.is_file():
result.append(str(file.relative_to(static_path)))
if file.is_dir() and file.name not in ('node_modules', 'src'):
# ignore "src" and "node_modules" directories
result.extend(get_static_files_legacy(static_path, file))
return result
def get_static_files(static_path: str) -> Dict[str, str]: def get_static_files(static_path: str) -> Dict[str, str]:
static_files: Dict[str, str] = {} results = {}
static_path_path = pathlib.Path(static_path) themes_dir = pathlib.Path(static_path) / "themes"
for theme_dir in themes_dir.iterdir():
if not theme_dir.is_dir():
continue
assets_file = theme_dir / "assets.json"
if assets_file.is_file():
# assets.json exist
with assets_file.open("r", encoding="utf-8") as f:
assets = json.load(f)
for rel_logical_filename, rel_actual_filename in assets.items():
logical_filename = f"themes/{theme_dir.name}/{rel_logical_filename}"
actual_filename = f"themes/{theme_dir.name}/{rel_actual_filename}"
results[logical_filename] = actual_filename
else:
# assets.json does not exist
results.update(
{
logical_filename: logical_filename
for logical_filename in get_static_files_legacy(static_path, theme_dir)
}
)
def walk(path: pathlib.Path): return results
for file in path.iterdir():
if file.name.startswith('.'):
# ignore hidden file
continue
if file.is_file():
static_files[str(file.relative_to(static_path_path))] = get_hash_for_file(file)
if file.is_dir() and file.name not in ('node_modules', 'src'):
# ignore "src" and "node_modules" directories
walk(file)
walk(static_path_path)
return static_files
def get_result_templates(templates_path): def get_result_templates(templates_path):

View File

@ -25,9 +25,6 @@ class ViewsTestCase(SearxTestCase): # pylint: disable=too-many-public-methods
pass pass
self.setattr4test(searx.search.processors, 'initialize_processor', dummy) self.setattr4test(searx.search.processors, 'initialize_processor', dummy)
# remove sha for the static file so the tests don't have to care about
# the changing URLs
self.setattr4test(searx.webapp, 'static_files', {})
# set some defaults # set some defaults
test_results = [ test_results = [

View File

@ -122,4 +122,5 @@ static.build.restore() {
build_msg STATIC "git-restore of the built files (/static)" build_msg STATIC "git-restore of the built files (/static)"
git restore --staged "${STATIC_BUILT_PATHS[@]}" git restore --staged "${STATIC_BUILT_PATHS[@]}"
git restore --worktree "${STATIC_BUILT_PATHS[@]}" git restore --worktree "${STATIC_BUILT_PATHS[@]}"
git clean --force -d "${STATIC_BUILT_PATHS[@]}"
} }