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 }