kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/go/services/filetree/filetree.go (about)

     1  /*
     2   * Copyright 2015 The Kythe Authors. All rights reserved.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *   http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  // Package filetree defines the filetree Service interface and a simple
    18  // in-memory implementation.
    19  package filetree // import "kythe.io/kythe/go/services/filetree"
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"net/http"
    25  	"path"
    26  	"path/filepath"
    27  	"strings"
    28  	"time"
    29  
    30  	"kythe.io/kythe/go/services/graphstore"
    31  	"kythe.io/kythe/go/services/web"
    32  	"kythe.io/kythe/go/util/log"
    33  	"kythe.io/kythe/go/util/schema/facts"
    34  	"kythe.io/kythe/go/util/schema/nodes"
    35  
    36  	"google.golang.org/protobuf/proto"
    37  
    38  	ftpb "kythe.io/kythe/proto/filetree_go_proto"
    39  	spb "kythe.io/kythe/proto/storage_go_proto"
    40  )
    41  
    42  // Service provides an interface to explore a tree of VName files.
    43  type Service interface {
    44  	// Directory returns the contents of the directory at the given corpus/root/path.
    45  	Directory(context.Context, *ftpb.DirectoryRequest) (*ftpb.DirectoryReply, error)
    46  
    47  	// CorpusRoots returns a map from corpus to known roots.
    48  	CorpusRoots(context.Context, *ftpb.CorpusRootsRequest) (*ftpb.CorpusRootsReply, error)
    49  
    50  	// Close releases any underlying resources.
    51  	Close(context.Context) error
    52  }
    53  
    54  // CleanDirPath returns a clean, corpus root relative equivalent to path.
    55  func CleanDirPath(path string) string {
    56  	const sep = string(filepath.Separator)
    57  	return strings.TrimPrefix(filepath.Join(sep, path), sep)
    58  }
    59  
    60  // Map is a FileTree backed by an in-memory map.
    61  type Map struct {
    62  	// corpus -> root -> dirPath -> DirectoryReply
    63  	M map[string]map[string]map[string]*ftpb.DirectoryReply
    64  }
    65  
    66  // NewMap returns an empty filetree map.
    67  func NewMap() *Map {
    68  	return &Map{make(map[string]map[string]map[string]*ftpb.DirectoryReply)}
    69  }
    70  
    71  // Populate adds each file node in gs to m.
    72  func (m *Map) Populate(ctx context.Context, gs graphstore.Service) error {
    73  	start := time.Now()
    74  	log.Info("Populating in-memory file tree")
    75  	var total int
    76  	if err := gs.Scan(ctx, &spb.ScanRequest{FactPrefix: facts.NodeKind},
    77  		func(entry *spb.Entry) error {
    78  			if entry.FactName == facts.NodeKind && string(entry.FactValue) == nodes.File {
    79  				m.AddFile(entry.Source)
    80  				total++
    81  			}
    82  			return nil
    83  		}); err != nil {
    84  		return fmt.Errorf("failed to Scan GraphStore for directory structure: %v", err)
    85  	}
    86  	log.InfoContextf(ctx, "Indexed %d files in %s", total, time.Since(start))
    87  	return nil
    88  }
    89  
    90  // AddFile adds the given file VName to m.
    91  func (m *Map) AddFile(file *spb.VName) {
    92  	dirPath := CleanDirPath(path.Dir(file.Path))
    93  	dir := m.ensureDir(file.Corpus, file.Root, dirPath)
    94  	dir.Entry = addEntry(dir.Entry, &ftpb.DirectoryReply_Entry{
    95  		Kind:      ftpb.DirectoryReply_FILE,
    96  		Name:      filepath.Base(file.Path),
    97  		Generated: file.GetRoot() != "",
    98  	})
    99  }
   100  
   101  // CorpusRoots implements part of the filetree.Service interface.
   102  func (m *Map) CorpusRoots(ctx context.Context, req *ftpb.CorpusRootsRequest) (*ftpb.CorpusRootsReply, error) {
   103  	cr := &ftpb.CorpusRootsReply{}
   104  	for corpus, rootDirs := range m.M {
   105  		var roots []string
   106  		for root := range rootDirs {
   107  			roots = append(roots, root)
   108  		}
   109  		cr.Corpus = append(cr.Corpus, &ftpb.CorpusRootsReply_Corpus{
   110  			Name: corpus,
   111  			Root: roots,
   112  		})
   113  	}
   114  	return cr, nil
   115  }
   116  
   117  // Directory implements part of the filetree.Service interface.
   118  func (m *Map) Directory(ctx context.Context, req *ftpb.DirectoryRequest) (*ftpb.DirectoryReply, error) {
   119  	roots := m.M[req.Corpus]
   120  	if roots == nil {
   121  		return &ftpb.DirectoryReply{}, nil
   122  	}
   123  	dirs := roots[req.Root]
   124  	if dirs == nil {
   125  		return &ftpb.DirectoryReply{}, nil
   126  	}
   127  	d := dirs[req.Path]
   128  	if d == nil {
   129  		return &ftpb.DirectoryReply{}, nil
   130  	}
   131  	return d, nil
   132  }
   133  
   134  func (m *Map) ensureCorpusRoot(corpus, root string) map[string]*ftpb.DirectoryReply {
   135  	roots := m.M[corpus]
   136  	if roots == nil {
   137  		roots = make(map[string]map[string]*ftpb.DirectoryReply)
   138  		m.M[corpus] = roots
   139  	}
   140  
   141  	dirs := roots[root]
   142  	if dirs == nil {
   143  		dirs = make(map[string]*ftpb.DirectoryReply)
   144  		roots[root] = dirs
   145  	}
   146  	return dirs
   147  }
   148  
   149  func (m *Map) ensureDir(corpus, root, path string) *ftpb.DirectoryReply {
   150  	if path == "." {
   151  		path = ""
   152  	}
   153  	dirs := m.ensureCorpusRoot(corpus, root)
   154  	dir := dirs[path]
   155  	if dir == nil {
   156  		dir = &ftpb.DirectoryReply{
   157  			Corpus: corpus,
   158  			Root:   root,
   159  			Path:   path,
   160  		}
   161  		dirs[path] = dir
   162  
   163  		if path != "" {
   164  			parent := m.ensureDir(corpus, root, filepath.Dir(path))
   165  			parent.Entry = addEntry(parent.Entry, &ftpb.DirectoryReply_Entry{
   166  				Kind:      ftpb.DirectoryReply_DIRECTORY,
   167  				Name:      filepath.Base(path),
   168  				Generated: root != "",
   169  			})
   170  		}
   171  	}
   172  	return dir
   173  }
   174  
   175  func addEntry(entries []*ftpb.DirectoryReply_Entry, e *ftpb.DirectoryReply_Entry) []*ftpb.DirectoryReply_Entry {
   176  	for _, x := range entries {
   177  		if proto.Equal(x, e) {
   178  			return entries
   179  		}
   180  	}
   181  	return append(entries, e)
   182  }
   183  
   184  type webClient struct{ addr string }
   185  
   186  func (webClient) Close(context.Context) error { return nil }
   187  
   188  // CorpusRoots implements part of the Service interface.
   189  func (w *webClient) CorpusRoots(ctx context.Context, req *ftpb.CorpusRootsRequest) (*ftpb.CorpusRootsReply, error) {
   190  	var reply ftpb.CorpusRootsReply
   191  	return &reply, web.Call(w.addr, "corpusRoots", req, &reply)
   192  }
   193  
   194  // Directory implements part of the Service interface.
   195  func (w *webClient) Directory(ctx context.Context, req *ftpb.DirectoryRequest) (*ftpb.DirectoryReply, error) {
   196  	var reply ftpb.DirectoryReply
   197  	return &reply, web.Call(w.addr, "dir", req, &reply)
   198  }
   199  
   200  // WebClient returns an filetree Service based on a remote web server.
   201  func WebClient(addr string) Service { return &webClient{addr} }
   202  
   203  // RegisterHTTPHandlers registers JSON HTTP handlers with mux using the given
   204  // filetree Service.  The following methods with be exposed:
   205  //
   206  //	GET /corpusRoots
   207  //	  Response: JSON encoded filetree.CorpusRootsReply
   208  //	GET /dir
   209  //	  Request: JSON encoded filetree.DirectoryRequest
   210  //	  Response: JSON encoded filetree.DirectoryReply
   211  //
   212  // Note: /corpusRoots and /dir will return their responses as serialized
   213  // protobufs if the "proto" query parameter is set.
   214  func RegisterHTTPHandlers(ctx context.Context, ft Service, mux *http.ServeMux) {
   215  	mux.HandleFunc("/corpusRoots", func(w http.ResponseWriter, r *http.Request) {
   216  		start := time.Now()
   217  		defer func() {
   218  			log.InfoContextf(ctx, "filetree.CorpusRoots:\t%s", time.Since(start))
   219  		}()
   220  
   221  		var req ftpb.CorpusRootsRequest
   222  		if err := web.ReadJSONBody(r, &req); err != nil {
   223  			http.Error(w, err.Error(), http.StatusBadRequest)
   224  			return
   225  		}
   226  		cr, err := ft.CorpusRoots(ctx, &req)
   227  		if err != nil {
   228  			http.Error(w, err.Error(), http.StatusInternalServerError)
   229  			return
   230  		}
   231  		if err := web.WriteResponse(w, r, cr); err != nil {
   232  			log.InfoContext(ctx, err)
   233  		}
   234  	})
   235  	mux.HandleFunc("/dir", func(w http.ResponseWriter, r *http.Request) {
   236  		start := time.Now()
   237  		defer func() {
   238  			log.InfoContextf(ctx, "filetree.Dir:\t%s", time.Since(start))
   239  		}()
   240  
   241  		var req ftpb.DirectoryRequest
   242  		if err := web.ReadJSONBody(r, &req); err != nil {
   243  			http.Error(w, err.Error(), http.StatusBadRequest)
   244  			return
   245  		}
   246  		reply, err := ft.Directory(ctx, &req)
   247  		if err != nil {
   248  			http.Error(w, err.Error(), http.StatusInternalServerError)
   249  			return
   250  		}
   251  		if err := web.WriteResponse(w, r, reply); err != nil {
   252  			log.InfoContext(ctx, err)
   253  		}
   254  	})
   255  }