|  | 
 
| 本帖最后由 sharp097 于 2023-11-19 20:52 编辑 
 如果希望看到更漂亮的排版,可以移步我下面的博客:
 
 https://www.sharpgan.com/alist-modification-to-support-grep-brute-force-search/
 
 # 前言
 
 就我个人而言,不管多么吊的全文搜索,总是很不靠谱,关键时候总是掉链子,即使是买了昂贵的所谓的企业级的Elasticsearch,其提供的全文搜索功能也是很不靠谱,毕竟在维护基于es的日志搜索平台时,每天都有太多的人找我投诉搜不到想要的日志,而一去机器上用grep搜索的时候就有,具体细节我就不赘述了,所以我现在我只相信grep暴力搜索,好了不废话了,下面进入正题。
 
 声明:
 
 下面的代码全部基于Alist的一个老版本做的改动,这个版本是Version: v3.11.0-0-gfe416ba-dirty,如果你需要基于最新版本的Alist修改的话,应该可以举一反三。
 
 修改完了之后参考下面的官方文档进行编译:
 
 [https://alist.nn.ci/zh/guide/install/source.html](https://alist.nn.ci/zh/guide/install/source.html)
 
 # 预览
 
 文件变动一览:
 
 #### 后端
 
 - 删除文件夹:internal/search/db_non_full_text 因为internal/bootstrap/data/setting.go中的key为conf.SearchIndex的options值中的database_non_full_text修改为了grep,所以这个文件夹就用不到了
 
 - 新增文件夹:internal/search/grep 这是新的grep搜索类型,要新增一个文件夹来实现
 
 新增文件:
 - internal/search/grep/search.go grep搜索的核心代码
 - internal/search/grep/init.go grep搜索的init代码
 
 修改文件:
 - internal/bootstrap/data/setting.go 用来把db_non_full_text逻辑替换成grep搜索逻辑
 - internal/search/import.go 用来导入新的grep搜索逻辑
 
 #### 前端
 新增:无
 
 修改文件:
 - src/pages/home/folder/Search.tsx 用来支持在新的标签页打开搜索结果中的html
 - src/pages/home/previews/index.ts 用来让html文件默认用html预览工具来打开
 
 # 开干
 
 玩的就是真实。
 
 ### 后端
 
 后端新增文件internal/search/grep/search.go文件内容如下:
 
 ```go
 package db
 
 import (
 "bufio"
 "context"
 "github.com/alist-org/alist/v3/drivers/local"
 "github.com/alist-org/alist/v3/internal/db"
 "github.com/alist-org/alist/v3/internal/model"
 "github.com/alist-org/alist/v3/internal/search/searcher"
 "github.com/alist-org/alist/v3/pkg/utils"
 "os/exec"
 "path/filepath"
 "strings"
 )
 
 type Grep struct{}
 
 func (D Grep) Config() searcher.Config {
 return config
 }
 
 func (D Grep) ListLocalStorage() []model.Storage {
 storages, _, err := db.GetStorages(0, 500)
 var localStorages []model.Storage
 if err != nil {
 return localStorages
 }
 
 for i := range storages {
 storage := storages
 if storage.Driver == "Local" {
 localStorages = append(localStorages, storage)
 }
 }
 return localStorages
 }
 
 func (D Grep) FindRealRoot(parentFolder string) (string, string) {
 if len(parentFolder) <= 0 {
 return "", ""
 }
 localStorages := D.ListLocalStorage()
 if len(localStorages) <= 0 {
 return "", ""
 }
 for i := range localStorages {
 localStorage := localStorages
 // Unmarshal Addition
 addition := &local.Addition{}
 err := utils.Json.UnmarshalFromString(localStorage.Addition, addition)
 if err != nil {
 continue
 }
 if strings.Contains(parentFolder, localStorage.MountPath) {
 return localStorage.MountPath, addition.RootFolderPath
 }
 }
 return "", ""
 }
 
 func (D Grep) Search(ctx context.Context, req model.SearchReq) ([]model.SearchNode, int64, error) {
 mountPath, rootFolderPath := D.FindRealRoot(req.Parent)
 if len(mountPath) == 0 || len(rootFolderPath) == 0 {
 return []model.SearchNode{}, 0, nil
 }
 
 realRootFolder := strings.Replace(
 req.Parent,
 mountPath,
 rootFolderPath,
 1,
 )
 kw:=req.Keywords
 isSpace:=strings.Contains(kw, " ")
 if isSpace == true {
 strSlice:=strings.Split(kw," ")
 formerStr:=strSlice[0]
 latterStr:=strSlice[1]
 kw=formerStr+".*"+latterStr+"|"+latterStr+".*"+formerStr
 }
 cmd := exec.Command("grep", "-R", "-l", "-i", "-E", kw, realRootFolder)
 
 stderr, _ := cmd.StdoutPipe()
 cmd.Start()
 
 scanner := bufio.NewScanner(stderr)
 scanner.Split(bufio.ScanLines)
 var fileList []model.SearchNode
 var limit int = 0
 for scanner.Scan() {
 m := scanner.Text()
 fileName := strings.Split(m, ":")[0]
 cdir, cfile := filepath.Split(fileName)
 cfile = strings.TrimSuffix(cfile, "/")
 cdir = strings.Replace(cdir, rootFolderPath, mountPath, 1)
 
 if itemExists(fileList, cdir, cfile) {
 continue
 }
 
 fileList = append(fileList, model.SearchNode{
 Parent: cdir,
 Name:   cfile,
 IsDir:  false,
 Size:   0,
 })
 limit++
 if limit >= 100 {
 break
 }
 }
 cmd.Wait()
 return fileList, 0, nil
 }
 
 func itemExists(fileList []model.SearchNode, cdir string, cfile string) bool {
 for i := range fileList {
 file := fileList
 if file.Parent == cdir && file.Name == cfile {
 return true
 }
 }
 return false
 }
 
 func (D Grep) Index(ctx context.Context, node model.SearchNode) error {
 return nil
 }
 
 func (D Grep) BatchIndex(ctx context.Context, nodes []model.SearchNode) error {
 return nil
 }
 
 func (D Grep) Get(ctx context.Context, parent string) ([]model.SearchNode, error) {
 return []model.SearchNode{}, nil
 }
 
 func (D Grep) Del(ctx context.Context, prefix string) error {
 return nil
 }
 
 func (D Grep) Release(ctx context.Context) error {
 return nil
 }
 
 func (D Grep) Clear(ctx context.Context) error {
 return nil
 }
 
 var _ searcher.Searcher = (*Grep)(nil)
 ```
 
 后端新增文件internal/search/grep/init.go文件内容如下:
 
 ```go
 package db
 
 import (
 "github.com/alist-org/alist/v3/internal/search/searcher"
 )
 
 var config = searcher.Config{
 Name:       "grep",
 AutoUpdate: true,
 }
 
 func init() {
 searcher.RegisterSearcher(config, func() (searcher.Searcher, error) {
 return &Grep{}, nil
 })
 }
 ```
 
 后端修改文件internal/bootstrap/data/setting.go具体为:
 
 把key为conf.SearchIndex的options值中的database_non_full_text修改为grep
 
 后端修改文件internal/search/import.go具体为:
 
 把db_non_full_text改为grep
 
 ### 前端
 
 前端修改文件src/pages/home/folder/Search.tsx具体为:
 
 在SearchResult这个函数下面的return属性中新增一个 `target="_blank"`属性,用来支持在新的标签页打开搜索结果中的html,示例代码如下:
 
 ```typescript
 const SearchResult = (props: SearchNode) => {
 return (
 <HStack
 w="$full"
 borderBottom={`1px solid ${hoverColor()}`}
 _hover={{
 bgColor: hoverColor(),
 }}
 rounded="$md"
 cursor="pointer"
 px="$2"
 as={LinkWithBase}
 href={props.path}
 target="_blank"
 encode
 >
 ```
 
 前端修改文件src/pages/home/previews/index.ts具体为:
 
 在函数getPreviews中将常量res改为变量:
 
 从原来的 `const res: PreviewComponent[] = []`变为 `var res: PreviewComponent[] = []`
 
 然后在// iframe previews注释上方新增如下代码,用来让html文件默认用html预览工具来打开:
 
 ```typescript
 var fileExt = ext(file.name).toLowerCase()
 if (fileExt == "html") {
 res = res.filter(item => item.name === "HTML render")
 }
 ```
 
 完整示例代码如下:
 
 ```typescript
 export const getPreviews = (
 file: Obj & { provider: string }
 ): PreviewComponent[] => {
 var res: PreviewComponent[] = []
 // internal previews
 previews.forEach((preview) => {
 if (preview.provider && !preview.provider.test(file.provider)) {
 return
 }
 if (
 preview.type === file.type ||
 preview.exts === "*" ||
 preview.exts?.includes(ext(file.name).toLowerCase())
 ) {
 res.push({ name: preview.name, component: preview.component })
 }
 })
 var fileExt = ext(file.name).toLowerCase()
 if (fileExt == "html") {
 res = res.filter(item => item.name === "HTML render")
 }
 // iframe previews
 const iframePreviews = getIframePreviews(file.name)
 iframePreviews.forEach((preview) => {
 res.push({
 name: preview.key,
 component: generateIframePreview(preview.value),
 })
 })
 // download page
 res.push({
 name: "Download",
 component: lazy(() => import("./download")),
 })
 return res
 }
 ```
 
 下面是如何实现支持中文:
 
 去[**https://crowdin.com/backend/download/project/alist/zh-CN.zip**](https://crowdin.com/backend/download/project/alist/zh-CN.zip)这里下载中文语言包,解压后放到前端文件夹src/lang下面,然后返回前端项目根目录执行 `node ./scripts/i18n.mjs,`
 
 此时src/lang/zh-CN下面会生成一个叫entry.ts的文件然后重新编译前端项目即可。
 
 | 
 |