fork of https://github.com/sourcegraph/zoekt
1// Copyright 2016 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package web
16
17import (
18 "bytes"
19 "context"
20 "encoding/json"
21 "fmt"
22 "html/template"
23 "log"
24 "net"
25 "net/http"
26 "regexp/syntax"
27 "sort"
28 "strconv"
29 "strings"
30 "sync"
31 "time"
32
33 "github.com/grafana/regexp"
34 "github.com/sourcegraph/zoekt"
35 zjson "github.com/sourcegraph/zoekt/json"
36 "github.com/sourcegraph/zoekt/query"
37)
38
39var Funcmap = template.FuncMap{
40 "Inc": func(orig int) int {
41 return orig + 1
42 },
43 "More": func(orig int) int {
44 return orig * 3
45 },
46 "HumanUnit": func(orig int64) string {
47 b := orig
48 suffix := ""
49 if orig > 10*(1<<30) {
50 suffix = "G"
51 b = orig / (1 << 30)
52 } else if orig > 10*(1<<20) {
53 suffix = "M"
54 b = orig / (1 << 20)
55 } else if orig > 10*(1<<10) {
56 suffix = "K"
57 b = orig / (1 << 10)
58 }
59
60 return fmt.Sprintf("%d%s", b, suffix)
61 },
62 "LimitPre": func(limit int, pre string) string {
63 if len(pre) < limit {
64 return pre
65 }
66 return fmt.Sprintf("...(%d bytes skipped)...%s", len(pre)-limit, pre[len(pre)-limit:])
67 },
68 "LimitPost": func(limit int, post string) string {
69 if len(post) < limit {
70 return post
71 }
72 return fmt.Sprintf("%s...(%d bytes skipped)...", post[:limit], len(post)-limit)
73 },
74 "TrimTrailingNewline": func(s string) string {
75 return strings.TrimSuffix(s, "\n")
76 },
77}
78
79const defaultNumResults = 50
80
81type Server struct {
82 Searcher zoekt.Streamer
83
84 // Serve HTML interface
85 HTML bool
86
87 // Serve RPC
88 RPC bool
89
90 // If set, show files from the index.
91 Print bool
92
93 // Version string for this server.
94 Version string
95
96 // Depending on the Host header, add a query to the entry
97 // page. For example, when serving on "search.myproject.org"
98 // we could add "r:myproject" automatically. This allows a
99 // single instance to serve as search engine for multiple
100 // domains.
101 HostCustomQueries map[string]string
102
103 // This should contain the following templates: "repolist"
104 // (for the repo search result page), "result" for
105 // the search results, "search" (for the opening page),
106 // "box" for the search query input element and
107 // "print" for the show file functionality.
108 Top *template.Template
109
110 repolist *template.Template
111 search *template.Template
112 result *template.Template
113 print *template.Template
114 about *template.Template
115 robots *template.Template
116
117 startTime time.Time
118
119 templateMu sync.Mutex
120 templateCache map[string]*template.Template
121
122 lastStatsMu sync.Mutex
123 lastStats *zoekt.RepoStats
124 lastStatsTS time.Time
125}
126
127func (s *Server) getTemplate(str string) *template.Template {
128 s.templateMu.Lock()
129 defer s.templateMu.Unlock()
130 t := s.templateCache[str]
131 if t != nil {
132 return t
133 }
134
135 t, err := template.New("cache").Parse(str)
136 if err != nil {
137 log.Printf("template parse error: %v", err)
138 t = template.Must(template.New("empty").Parse(""))
139 }
140 s.templateCache[str] = t
141 return t
142}
143
144func NewMux(s *Server) (*http.ServeMux, error) {
145 s.print = s.Top.Lookup("print")
146 if s.print == nil {
147 return nil, fmt.Errorf("missing template 'print'")
148 }
149
150 for k, v := range map[string]**template.Template{
151 "results": &s.result,
152 "print": &s.print,
153 "search": &s.search,
154 "repolist": &s.repolist,
155 "about": &s.about,
156 "robots": &s.robots,
157 } {
158 *v = s.Top.Lookup(k)
159 if *v == nil {
160 return nil, fmt.Errorf("missing template %q", k)
161 }
162 }
163
164 s.templateCache = map[string]*template.Template{}
165 s.startTime = time.Now()
166
167 mux := http.NewServeMux()
168
169 if s.HTML {
170 mux.HandleFunc("/robots.txt", s.serveRobots)
171 mux.HandleFunc("/search", s.serveSearch)
172 mux.HandleFunc("/", s.serveSearchBox)
173 mux.HandleFunc("/about", s.serveAbout)
174 mux.HandleFunc("/print", s.servePrint)
175 }
176 if s.RPC {
177 mux.Handle("/api/", http.StripPrefix("/api", zjson.JSONServer(traceAwareSearcher{s.Searcher})))
178 }
179
180 mux.HandleFunc("/healthz", s.serveHealthz)
181
182 return mux, nil
183}
184
185func (s *Server) serveHealthz(w http.ResponseWriter, r *http.Request) {
186 q := &query.Const{Value: true}
187 opts := &zoekt.SearchOptions{ShardMaxMatchCount: 1, TotalMaxMatchCount: 1, MaxDocDisplayCount: 1}
188
189 result, err := s.Searcher.Search(r.Context(), q, opts)
190 if err != nil {
191 http.Error(w, fmt.Sprintf("not ready: %v", err), http.StatusInternalServerError)
192 return
193 }
194
195 w.Header().Set("Content-Type", "application/json")
196
197 _ = json.NewEncoder(w).Encode(result)
198}
199
200func (s *Server) serveSearch(w http.ResponseWriter, r *http.Request) {
201 result, err := s.serveSearchErr(r)
202 if err != nil {
203 http.Error(w, err.Error(), http.StatusTeapot)
204 return
205 }
206
207 qvals := r.URL.Query()
208 if qvals.Get("format") == "json" {
209 w.Header().Add("Content-Type", "application/json")
210 encoder := json.NewEncoder(w)
211 encoder.Encode(result)
212 return
213 }
214
215 var buf bytes.Buffer
216 if result.Repos != nil {
217 err = s.repolist.Execute(&buf, &result.Repos)
218 } else if result.Result != nil {
219 err = s.result.Execute(&buf, &result.Result)
220 }
221 if err != nil {
222 http.Error(w, err.Error(), http.StatusTeapot)
223 return
224 }
225 w.Write(buf.Bytes())
226}
227
228func (s *Server) serveSearchErr(r *http.Request) (*ApiSearchResult, error) {
229 qvals := r.URL.Query()
230
231 debugScore, _ := strconv.ParseBool(qvals.Get("debug"))
232
233 queryStr := qvals.Get("q")
234 if queryStr == "" {
235 return nil, fmt.Errorf("no query found")
236 }
237
238 q, err := query.Parse(queryStr)
239 if err != nil {
240 return nil, err
241 }
242
243 // Experimental: The query string and boost exact phrases of it.
244 if phraseBoost, err := strconv.ParseFloat(qvals.Get("phrase-boost"), 64); err == nil {
245 q = query.ExpirementalPhraseBoost(q, queryStr, query.ExperimentalPhraseBoostOptions{
246 Boost: phraseBoost,
247 })
248 }
249
250 repoOnly := true
251 query.VisitAtoms(q, func(q query.Q) {
252 _, ok := q.(*query.Repo)
253 repoOnly = repoOnly && ok
254 })
255 if repoOnly {
256 repos, err := s.serveListReposErr(q, queryStr, r)
257 if err == nil {
258 return &ApiSearchResult{Repos: repos}, nil
259 }
260 return nil, err
261 }
262
263 if qt, ok := q.(*query.Type); ok && qt.Type == query.TypeRepo {
264 repos, err := s.serveListReposErr(q, queryStr, r)
265 if err == nil {
266 return &ApiSearchResult{Repos: repos}, nil
267 }
268 return nil, err
269 }
270
271 numStr := qvals.Get("num")
272
273 num, err := strconv.Atoi(numStr)
274 if err != nil || num <= 0 {
275 num = defaultNumResults
276 }
277
278 sOpts := zoekt.SearchOptions{
279 MaxWallTime: 10 * time.Second,
280 }
281
282 numCtxLines := 0
283 if qvals.Get("format") == "json" {
284 if ctxLinesStr := qvals.Get("ctx"); ctxLinesStr != "" {
285 numCtxLines, err = strconv.Atoi(ctxLinesStr)
286 if err != nil || numCtxLines < 0 || numCtxLines > 10 {
287 return nil, fmt.Errorf("Number of context lines must be between 0 and 10")
288 }
289 }
290 }
291 sOpts.NumContextLines = numCtxLines
292
293 sOpts.SetDefaults()
294 sOpts.MaxDocDisplayCount = num
295 sOpts.DebugScore = debugScore
296
297 ctx := r.Context()
298 if err := zjson.CalculateDefaultSearchLimits(ctx, q, s.Searcher, &sOpts); err != nil {
299 return nil, err
300 }
301
302 result, err := s.Searcher.Search(ctx, q, &sOpts)
303 if err != nil {
304 return nil, err
305 }
306
307 fileMatches, err := s.formatResults(result, queryStr, s.Print)
308 if err != nil {
309 return nil, err
310 }
311
312 res := ResultInput{
313 Last: LastInput{
314 Query: queryStr,
315 Num: num,
316 AutoFocus: true,
317 },
318 Stats: result.Stats,
319 Query: q.String(),
320 QueryStr: queryStr,
321 FileMatches: fileMatches,
322 }
323 if res.Stats.Wait < res.Stats.Duration/10 {
324 // Suppress queueing stats if they are neglible.
325 res.Stats.Wait = 0
326 }
327
328 res.Last.Debug = debugScore
329 return &ApiSearchResult{Result: &res}, nil
330}
331
332func (s *Server) servePrint(w http.ResponseWriter, r *http.Request) {
333 err := s.servePrintErr(w, r)
334 if err != nil {
335 http.Error(w, err.Error(), http.StatusTeapot)
336 }
337}
338
339const statsStaleNess = 30 * time.Second
340
341func (s *Server) fetchStats(ctx context.Context) (*zoekt.RepoStats, error) {
342 s.lastStatsMu.Lock()
343 stats := s.lastStats
344 if time.Since(s.lastStatsTS) > statsStaleNess {
345 stats = nil
346 }
347 s.lastStatsMu.Unlock()
348
349 if stats != nil {
350 return stats, nil
351 }
352
353 repos, err := s.Searcher.List(ctx, &query.Const{Value: true}, nil)
354 if err != nil {
355 return nil, err
356 }
357
358 stats = &repos.Stats
359
360 s.lastStatsMu.Lock()
361 s.lastStatsTS = time.Now()
362 s.lastStats = stats
363 s.lastStatsMu.Unlock()
364
365 return stats, nil
366}
367
368func (s *Server) serveSearchBoxErr(w http.ResponseWriter, r *http.Request) error {
369 stats, err := s.fetchStats(r.Context())
370 if err != nil {
371 return err
372 }
373 d := SearchBoxInput{
374 Last: LastInput{
375 Num: defaultNumResults,
376 AutoFocus: true,
377 },
378 Stats: stats,
379 Version: s.Version,
380 Uptime: time.Since(s.startTime),
381 }
382
383 d.Last.Query = r.URL.Query().Get("q")
384 if d.Last.Query == "" {
385 custom := s.HostCustomQueries[r.Host]
386 if custom == "" {
387 host, _, _ := net.SplitHostPort(r.Host)
388 custom = s.HostCustomQueries[host]
389 }
390
391 if custom != "" {
392 d.Last.Query = custom + " "
393 }
394 }
395
396 var buf bytes.Buffer
397 if err := s.search.Execute(&buf, &d); err != nil {
398 return err
399 }
400 _, _ = w.Write(buf.Bytes())
401 return nil
402}
403
404func (s *Server) serveSearchBox(w http.ResponseWriter, r *http.Request) {
405 if err := s.serveSearchBoxErr(w, r); err != nil {
406 http.Error(w, err.Error(), http.StatusTeapot)
407 }
408}
409
410func (s *Server) serveAboutErr(w http.ResponseWriter, r *http.Request) error {
411 stats, err := s.fetchStats(r.Context())
412 if err != nil {
413 return err
414 }
415
416 d := SearchBoxInput{
417 Stats: stats,
418 Version: s.Version,
419 Uptime: time.Since(s.startTime),
420 }
421
422 var buf bytes.Buffer
423 if err := s.about.Execute(&buf, &d); err != nil {
424 return err
425 }
426 _, _ = w.Write(buf.Bytes())
427 return nil
428}
429
430func (s *Server) serveAbout(w http.ResponseWriter, r *http.Request) {
431 if err := s.serveAboutErr(w, r); err != nil {
432 http.Error(w, err.Error(), http.StatusTeapot)
433 }
434}
435
436func (s *Server) serveRobotsErr(w http.ResponseWriter, r *http.Request) error {
437 data := struct{}{}
438 var buf bytes.Buffer
439 if err := s.robots.Execute(&buf, &data); err != nil {
440 return err
441 }
442 _, _ = w.Write(buf.Bytes())
443 return nil
444}
445
446func (s *Server) serveRobots(w http.ResponseWriter, r *http.Request) {
447 if err := s.serveRobotsErr(w, r); err != nil {
448 http.Error(w, err.Error(), http.StatusTeapot)
449 }
450}
451
452func (s *Server) serveListReposErr(q query.Q, qStr string, r *http.Request) (*RepoListInput, error) {
453 ctx := r.Context()
454 repos, err := s.Searcher.List(ctx, q, nil)
455 if err != nil {
456 return nil, err
457 }
458
459 qvals := r.URL.Query()
460 order := qvals.Get("order")
461 switch order {
462 case "", "name", "revname":
463 sort.Slice(repos.Repos, func(i, j int) bool {
464 return strings.Compare(repos.Repos[i].Repository.Name, repos.Repos[j].Repository.Name) < 0
465 })
466 case "size", "revsize":
467 sort.Slice(repos.Repos, func(i, j int) bool {
468 return repos.Repos[i].Stats.ContentBytes < repos.Repos[j].Stats.ContentBytes
469 })
470 case "ram", "revram":
471 sort.Slice(repos.Repos, func(i, j int) bool {
472 return repos.Repos[i].Stats.IndexBytes < repos.Repos[j].Stats.IndexBytes
473 })
474 case "time", "revtime":
475 sort.Slice(repos.Repos, func(i, j int) bool {
476 return repos.Repos[i].IndexMetadata.IndexTime.Before(
477 repos.Repos[j].IndexMetadata.IndexTime)
478 })
479 default:
480 return nil, fmt.Errorf("got unknown sort key %q, allowed [rev]name, [rev]time, [rev]size", order)
481 }
482 if strings.HasPrefix(order, "rev") {
483 for i, j := 0, len(repos.Repos)-1; i < j; {
484 repos.Repos[i], repos.Repos[j] = repos.Repos[j], repos.Repos[i]
485 i++
486 j--
487
488 }
489 }
490
491 aggregate := zoekt.RepoStats{
492 Repos: len(repos.Repos),
493 }
494 for _, s := range repos.Repos {
495 aggregate.Add(&s.Stats)
496 }
497
498 numStr := qvals.Get("num")
499 num, err := strconv.Atoi(numStr)
500 if err != nil || num <= 0 {
501 num = 0
502 }
503 if num > 0 {
504 if num > len(repos.Repos) {
505 num = len(repos.Repos)
506 }
507
508 repos.Repos = repos.Repos[:num]
509 }
510
511 res := RepoListInput{
512 Last: LastInput{
513 Query: qStr,
514 Num: num,
515 AutoFocus: true,
516 },
517 Stats: aggregate,
518 }
519
520 for _, r := range repos.Repos {
521 t := s.getTemplate(r.Repository.CommitURLTemplate)
522
523 repo := Repository{
524 Name: r.Repository.Name,
525 URL: r.Repository.URL,
526 IndexTime: r.IndexMetadata.IndexTime,
527 Size: r.Stats.ContentBytes,
528 MemorySize: r.Stats.IndexBytes,
529 Files: int64(r.Stats.Documents),
530 }
531 for _, b := range r.Repository.Branches {
532 var buf bytes.Buffer
533 if err := t.Execute(&buf, b); err != nil {
534 return nil, err
535 }
536 repo.Branches = append(repo.Branches,
537 Branch{
538 Name: b.Name,
539 Version: b.Version,
540 URL: buf.String(),
541 })
542 }
543 res.Repos = append(res.Repos, repo)
544 }
545 return &res, nil
546}
547
548func (s *Server) servePrintErr(w http.ResponseWriter, r *http.Request) error {
549 qvals := r.URL.Query()
550 fileStr := qvals.Get("f")
551 repoStr := qvals.Get("r")
552 queryStr := qvals.Get("q")
553 numStr := qvals.Get("num")
554 num, err := strconv.Atoi(numStr)
555 if err != nil || num <= 0 {
556 num = defaultNumResults
557 }
558
559 re, err := syntax.Parse("^"+regexp.QuoteMeta(fileStr)+"$", 0)
560 if err != nil {
561 return err
562 }
563
564 repoRe, err := regexp.Compile("^" + regexp.QuoteMeta(repoStr) + "$")
565 if err != nil {
566 return err
567 }
568
569 qs := []query.Q{
570 &query.Regexp{Regexp: re, FileName: true, CaseSensitive: true},
571 &query.Repo{Regexp: repoRe},
572 }
573
574 if branchStr := qvals.Get("b"); branchStr != "" {
575 qs = append(qs, &query.Branch{Pattern: branchStr})
576 }
577
578 q := &query.And{Children: qs}
579
580 sOpts := zoekt.SearchOptions{
581 Whole: true,
582 }
583
584 ctx := r.Context()
585 result, err := s.Searcher.Search(ctx, q, &sOpts)
586 if err != nil {
587 return err
588 }
589
590 if len(result.Files) != 1 {
591 var ss []string
592 for _, n := range result.Files {
593 ss = append(ss, n.FileName)
594 }
595 return fmt.Errorf("ambiguous result: %v", ss)
596 }
597
598 f := result.Files[0]
599
600 byteLines := bytes.Split(f.Content, []byte{'\n'})
601 strLines := make([]string, 0, len(byteLines))
602 for _, l := range byteLines {
603 strLines = append(strLines, string(l))
604 }
605
606 d := PrintInput{
607 Name: f.FileName,
608 Repo: f.Repository,
609 Lines: strLines,
610 Last: LastInput{
611 Query: queryStr,
612 Num: num,
613 AutoFocus: false,
614 },
615 }
616
617 var buf bytes.Buffer
618 if err := s.print.Execute(&buf, &d); err != nil {
619 return err
620 }
621
622 _, _ = w.Write(buf.Bytes())
623 return nil
624}