@@ -6,6 +6,7 @@ import { logger } from "../../../utils/logger";
66import {
77 SEARX_CATALOG ,
88 SEARX_SOURCE_BASE_URL ,
9+ SEARX_TRAITS_URL ,
910 catalogDeps ,
1011 catalogEntry ,
1112 dependants ,
@@ -19,6 +20,8 @@ import { LIB_PACKAGES, missingPythonLibs, type PythonLib } from "./python-deps";
1920const NS = "searx-install" ;
2021const PYCACHE_DIR = "__pycache__" ;
2122const DOWNLOAD_TIMEOUT_MS = 20_000 ;
23+ const TRAITS_SUFFIX = ".traits.json" ;
24+ const SHORTEST_ALIAS = 4 ;
2225
2326let _queue : Promise < unknown > = Promise . resolve ( ) ;
2427
@@ -61,21 +64,72 @@ const _download = async (code: string): Promise<string> => {
6164const _missingDeps = ( code : string ) : string [ ] =>
6265 catalogDeps ( code ) . filter ( ( dep ) => ! _isInstalled ( dep ) ) ;
6366
64- const _fetchFile = async ( code : string , dir : string ) : Promise < void > => {
65- const target = _enginePath ( code ) ;
67+ const _writeSwap = async ( target : string , body : string ) : Promise < void > => {
6668 const tmp = `${ target } .${ process . pid } .${ randomBytes ( 6 ) . toString ( "hex" ) } .tmp` ;
6769 try {
68- const source = await _download ( code ) ;
69- await mkdir ( dir , { recursive : true } ) ;
70- await writeFile ( tmp , source , "utf-8" ) ;
70+ await mkdir ( resolve ( searxEnginesDir ( ) ) , { recursive : true } ) ;
71+ await writeFile ( tmp , body , "utf-8" ) ;
7172 await rename ( tmp , target ) ;
72- await _dropCache ( code ) ;
7373 } catch ( err ) {
7474 await unlink ( tmp ) . catch ( ( ) => undefined ) ;
7575 throw err ;
7676 }
7777} ;
7878
79+ const _fetchFile = async ( code : string , dir : string ) : Promise < void > => {
80+ const source = await _download ( code ) ;
81+ await mkdir ( dir , { recursive : true } ) ;
82+ await _writeSwap ( _enginePath ( code ) , source ) ;
83+ await _dropCache ( code ) ;
84+ } ;
85+
86+ const _traitsPath = ( code : string ) : string =>
87+ join ( resolve ( searxEnginesDir ( ) ) , `${ code } ${ TRAITS_SUFFIX } ` ) ;
88+
89+ const _slug = ( value : string ) : string => value . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 ] / g, "" ) ;
90+
91+ const _traitsKey = ( keys : readonly string [ ] , code : string ) : string | undefined => {
92+ const want = _slug ( code ) ;
93+ const exact = keys . find ( ( key ) => _slug ( key ) === want ) ;
94+ if ( exact ) return exact ;
95+ const shorter = keys . find ( ( key ) => {
96+ const other = _slug ( key ) ;
97+ return other . length >= SHORTEST_ALIAS && want . startsWith ( other ) ;
98+ } ) ;
99+ if ( shorter ) return shorter ;
100+ return keys . find (
101+ ( key ) => want . length >= SHORTEST_ALIAS && _slug ( key ) . startsWith ( want ) ,
102+ ) ;
103+ } ;
104+
105+ const _traitsBook = async ( ) : Promise < Record < string , unknown > > => {
106+ const resp = await fetch ( SEARX_TRAITS_URL , {
107+ signal : AbortSignal . timeout ( DOWNLOAD_TIMEOUT_MS ) ,
108+ } ) ;
109+ if ( ! resp . ok ) throw new Error ( `Traits download failed with HTTP ${ resp . status } ` ) ;
110+ const book : unknown = await resp . json ( ) ;
111+ if ( ! book || typeof book !== "object" ) throw new Error ( "Traits file was not an object" ) ;
112+ return book as Record < string , unknown > ;
113+ } ;
114+
115+ const _saveTraits = async ( code : string , book : Record < string , unknown > ) : Promise < void > => {
116+ const key = _traitsKey ( Object . keys ( book ) , code ) ;
117+ const entry = key ? book [ key ] : undefined ;
118+ await _writeSwap ( _traitsPath ( code ) , JSON . stringify ( entry ?? { } ) ) ;
119+ } ;
120+
121+ const _pullTraits = async ( codes : readonly string [ ] ) : Promise < void > => {
122+ if ( codes . length === 0 ) return ;
123+ try {
124+ const book = await _traitsBook ( ) ;
125+ for ( const code of codes ) await _saveTraits ( code , book ) ;
126+ logger . debug ( NS , `stored SearX traits for ${ codes . join ( ", " ) } ` ) ;
127+ } catch ( err ) {
128+ const message = err instanceof Error ? err . message : String ( err ) ;
129+ logger . warn ( NS , `could not fetch SearX engine traits: ${ message } ` ) ;
130+ }
131+ } ;
132+
79133const _orphanDeps = ( code : string ) : string [ ] =>
80134 catalogDeps ( code ) . filter (
81135 ( dep ) =>
@@ -113,6 +167,7 @@ const _pull = async (
113167 const dir = resolve ( searxEnginesDir ( ) ) ;
114168 try {
115169 for ( const file of queue ) await _fetchFile ( file , dir ) ;
170+ await _pullTraits ( queue ) ;
116171 logger . info ( NS , `${ verb } SearX engine ${ engine } (${ queue . join ( ", " ) } )` ) ;
117172 } catch ( err ) {
118173 const message = err instanceof Error ? err . message : String ( err ) ;
@@ -140,6 +195,7 @@ export const uninstallSearx = async (code: string): Promise<void> => {
140195 try {
141196 for ( const file of queue ) {
142197 await unlink ( _enginePath ( file ) ) ;
198+ await unlink ( _traitsPath ( file ) ) . catch ( ( ) => undefined ) ;
143199 await _dropCache ( file ) ;
144200 }
145201 logger . info ( NS , `uninstalled SearX engine ${ engine } (${ queue . join ( ", " ) } )` ) ;
0 commit comments