github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/pkg/server/ui.go (about) 1 /* 2 Copyright 2011 Google Inc. 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 server 18 19 import ( 20 "errors" 21 "fmt" 22 "log" 23 "net/http" 24 "os" 25 "path" 26 "path/filepath" 27 "regexp" 28 "strconv" 29 "strings" 30 "time" 31 32 "camlistore.org/pkg/blob" 33 "camlistore.org/pkg/blobserver" 34 "camlistore.org/pkg/constants" 35 "camlistore.org/pkg/fileembed" 36 "camlistore.org/pkg/httputil" 37 "camlistore.org/pkg/jsonconfig" 38 "camlistore.org/pkg/jsonsign/signhandler" 39 "camlistore.org/pkg/misc/closure" 40 "camlistore.org/pkg/search" 41 "camlistore.org/pkg/sorted" 42 "camlistore.org/pkg/syncutil" 43 uistatic "camlistore.org/server/camlistored/ui" 44 closurestatic "camlistore.org/server/camlistored/ui/closure" 45 "camlistore.org/third_party/code.google.com/p/rsc/qr" 46 fontawesomestatic "camlistore.org/third_party/fontawesome" 47 glitchstatic "camlistore.org/third_party/glitch" 48 reactstatic "camlistore.org/third_party/react" 49 ) 50 51 var ( 52 staticFilePattern = regexp.MustCompile(`^([a-zA-Z0-9\-\_]+\.(html|js|css|png|jpg|gif|svg))$`) 53 identOrDotPattern = regexp.MustCompile(`^[a-zA-Z\_]+(\.[a-zA-Z\_]+)*$`) 54 55 // Download URL suffix: 56 // $1: blobref (checked in download handler) 57 // $2: optional "/filename" to be sent as recommended download name, 58 // if sane looking 59 downloadPattern = regexp.MustCompile(`^download/([^/]+)(/.*)?$`) 60 61 thumbnailPattern = regexp.MustCompile(`^thumbnail/([^/]+)(/.*)?$`) 62 treePattern = regexp.MustCompile(`^tree/([^/]+)(/.*)?$`) 63 closurePattern = regexp.MustCompile(`^closure/(([^/]+)(/.*)?)$`) 64 reactPattern = regexp.MustCompile(`^react/(.+)$`) 65 fontawesomePattern = regexp.MustCompile(`^fontawesome/(.+)$`) 66 glitchPattern = regexp.MustCompile(`^glitch/(.+)$`) 67 68 disableThumbCache, _ = strconv.ParseBool(os.Getenv("CAMLI_DISABLE_THUMB_CACHE")) 69 ) 70 71 // UIHandler handles serving the UI and discovery JSON. 72 type UIHandler struct { 73 // JSONSignRoot is the optional path or full URL to the JSON 74 // Signing helper. Only used by the UI and thus necessary if 75 // UI is true. 76 // TODO(bradfitz): also move this up to the root handler, 77 // if we start having clients (like phones) that we want to upload 78 // but don't trust to have private signing keys? 79 JSONSignRoot string 80 81 publishRoots map[string]*PublishHandler 82 83 prefix string // of the UI handler itself 84 root *RootHandler 85 sigh *signhandler.Handler // or nil 86 87 // Cache optionally specifies a cache blob server, used for 88 // caching image thumbnails and other emphemeral data. 89 Cache blobserver.Storage // or nil 90 91 // Limit peak RAM used by concurrent image thumbnail calls. 92 resizeSem *syncutil.Sem 93 thumbMeta *thumbMeta // optional thumbnail key->blob.Ref cache 94 95 // sourceRoot optionally specifies the path to root of Camlistore's 96 // source. If empty, the UI files must be compiled in to the 97 // binary (with go run make.go). This comes from the "sourceRoot" 98 // ui handler config option. 99 sourceRoot string 100 101 uiDir string // if sourceRoot != "", this is sourceRoot+"/server/camlistored/ui" 102 103 closureHandler http.Handler 104 fileReactHandler http.Handler 105 fileFontawesomeHandler http.Handler 106 fileGlitchHandler http.Handler 107 } 108 109 func init() { 110 blobserver.RegisterHandlerConstructor("ui", uiFromConfig) 111 } 112 113 // newKVOrNil wraps sorted.NewKeyValue and adds the ability 114 // to pass a nil conf to get a (nil, nil) response. 115 func newKVOrNil(conf jsonconfig.Obj) (sorted.KeyValue, error) { 116 if len(conf) == 0 { 117 return nil, nil 118 } 119 return sorted.NewKeyValue(conf) 120 } 121 122 func uiFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, err error) { 123 ui := &UIHandler{ 124 prefix: ld.MyPrefix(), 125 JSONSignRoot: conf.OptionalString("jsonSignRoot", ""), 126 sourceRoot: conf.OptionalString("sourceRoot", ""), 127 resizeSem: syncutil.NewSem(int64(conf.OptionalInt("maxResizeBytes", 128 constants.DefaultMaxResizeMem))), 129 } 130 pubRoots := conf.OptionalList("publishRoots") 131 cachePrefix := conf.OptionalString("cache", "") 132 scaledImageConf := conf.OptionalObject("scaledImage") 133 if err = conf.Validate(); err != nil { 134 return 135 } 136 137 if ui.JSONSignRoot != "" { 138 h, _ := ld.GetHandler(ui.JSONSignRoot) 139 if sigh, ok := h.(*signhandler.Handler); ok { 140 ui.sigh = sigh 141 } 142 } 143 144 if os.Getenv("CAMLI_PUBLISH_ENABLED") == "false" { 145 // Hack for dev server, to simplify its config with devcam server --publish=false. 146 pubRoots = nil 147 } 148 149 ui.publishRoots = make(map[string]*PublishHandler) 150 for _, pubRoot := range pubRoots { 151 h, err := ld.GetHandler(pubRoot) 152 if err != nil { 153 return nil, fmt.Errorf("UI handler's publishRoots references invalid %q", pubRoot) 154 } 155 pubh, ok := h.(*PublishHandler) 156 if !ok { 157 return nil, fmt.Errorf("UI handler's publishRoots references invalid %q; not a PublishHandler", pubRoot) 158 } 159 ui.publishRoots[pubRoot] = pubh 160 } 161 162 checkType := func(key string, htype string) { 163 v := conf.OptionalString(key, "") 164 if v == "" { 165 return 166 } 167 ct := ld.GetHandlerType(v) 168 if ct == "" { 169 err = fmt.Errorf("UI handler's %q references non-existant %q", key, v) 170 } else if ct != htype { 171 err = fmt.Errorf("UI handler's %q references %q of type %q; expected type %q", key, v, ct, htype) 172 } 173 } 174 checkType("searchRoot", "search") 175 checkType("jsonSignRoot", "jsonsign") 176 if err != nil { 177 return 178 } 179 180 scaledImageKV, err := newKVOrNil(scaledImageConf) 181 if err != nil { 182 return nil, fmt.Errorf("in UI handler's scaledImage: %v", err) 183 } 184 if scaledImageKV != nil && cachePrefix == "" { 185 return nil, fmt.Errorf("in UI handler, can't specify scaledImage without cache") 186 } 187 if cachePrefix != "" { 188 bs, err := ld.GetStorage(cachePrefix) 189 if err != nil { 190 return nil, fmt.Errorf("UI handler's cache of %q error: %v", cachePrefix, err) 191 } 192 ui.Cache = bs 193 ui.thumbMeta = newThumbMeta(scaledImageKV) 194 } 195 196 if ui.sourceRoot == "" { 197 ui.sourceRoot = os.Getenv("CAMLI_DEV_CAMLI_ROOT") 198 if uistatic.IsAppEngine { 199 if _, err = os.Stat(filepath.Join(uistatic.GaeSourceRoot, 200 filepath.FromSlash("server/camlistored/ui/index.html"))); err != nil { 201 hint := fmt.Sprintf("\"sourceRoot\" was not specified in the config,"+ 202 " and the default sourceRoot dir %v does not exist or does not contain"+ 203 " \"server/camlistored/ui/index.html\". devcam appengine can do that for you.", 204 uistatic.GaeSourceRoot) 205 log.Print(hint) 206 return nil, errors.New("No sourceRoot found; UI not available.") 207 } 208 log.Printf("Using the default \"%v\" as the sourceRoot for AppEngine", uistatic.GaeSourceRoot) 209 ui.sourceRoot = uistatic.GaeSourceRoot 210 } 211 } 212 if ui.sourceRoot != "" { 213 ui.uiDir = filepath.Join(ui.sourceRoot, filepath.FromSlash("server/camlistored/ui")) 214 // Ignore any fileembed files: 215 Files = &fileembed.Files{ 216 DirFallback: filepath.Join(ui.sourceRoot, filepath.FromSlash("pkg/server")), 217 } 218 uistatic.Files = &fileembed.Files{ 219 DirFallback: ui.uiDir, 220 Listable: true, 221 // In dev_appserver, allow edit-and-reload without 222 // restarting. In production, though, it's faster to just 223 // slurp it in. 224 SlurpToMemory: uistatic.IsProdAppEngine, 225 } 226 } 227 228 ui.closureHandler, err = ui.makeClosureHandler(ui.sourceRoot) 229 if err != nil { 230 return nil, fmt.Errorf(`Invalid "sourceRoot" value of %q: %v"`, ui.sourceRoot, err) 231 } 232 233 if ui.sourceRoot != "" { 234 ui.fileReactHandler, err = makeFileServer(ui.sourceRoot, filepath.Join("third_party", "react"), "react.js") 235 if err != nil { 236 return nil, fmt.Errorf("Could not make react handler: %s", err) 237 } 238 ui.fileGlitchHandler, err = makeFileServer(ui.sourceRoot, filepath.Join("third_party", "glitch"), "npc_piggy__x1_walk_png_1354829432.png") 239 if err != nil { 240 return nil, fmt.Errorf("Could not make glitch handler: %s", err) 241 } 242 ui.fileFontawesomeHandler, err = makeFileServer(ui.sourceRoot, filepath.Join("third_party", "fontawesome"), "css/font-awesome.css") 243 if err != nil { 244 return nil, fmt.Errorf("Could not make fontawesome handler: %s", err) 245 } 246 } 247 248 rootPrefix, _, err := ld.FindHandlerByType("root") 249 if err != nil { 250 return nil, errors.New("No root handler configured, which is necessary for the ui handler") 251 } 252 if h, err := ld.GetHandler(rootPrefix); err == nil { 253 ui.root = h.(*RootHandler) 254 ui.root.registerUIHandler(ui) 255 } else { 256 return nil, errors.New("failed to find the 'root' handler") 257 } 258 259 return ui, nil 260 } 261 262 func (ui *UIHandler) makeClosureHandler(root string) (http.Handler, error) { 263 return makeClosureHandler(root, "ui") 264 } 265 266 // makeClosureHandler returns a handler to serve Closure files. 267 // root is either: 268 // 1) empty: use the Closure files compiled in to the binary (if 269 // available), else redirect to the Internet. 270 // 2) a URL prefix: base of Camlistore to get Closure to redirect to 271 // 3) a path on disk to the root of camlistore's source (which 272 // contains the necessary subset of Closure files) 273 func makeClosureHandler(root, handlerName string) (http.Handler, error) { 274 // devcam server environment variable takes precedence: 275 if d := os.Getenv("CAMLI_DEV_CLOSURE_DIR"); d != "" { 276 log.Printf("%v: serving Closure from devcam server's $CAMLI_DEV_CLOSURE_DIR: %v", handlerName, d) 277 return http.FileServer(http.Dir(d)), nil 278 } 279 if root == "" { 280 fs, err := closurestatic.FileSystem() 281 if err == os.ErrNotExist { 282 log.Printf("%v: no configured setting or embedded resources; serving Closure via %v", handlerName, closureBaseURL) 283 return closureBaseURL, nil 284 } 285 if err != nil { 286 return nil, fmt.Errorf("error loading embedded Closure zip file: %v", err) 287 } 288 log.Printf("%v: serving Closure from embedded resources", handlerName) 289 return http.FileServer(fs), nil 290 } 291 if strings.HasPrefix(root, "http") { 292 log.Printf("%v: serving Closure using redirects to %v", handlerName, root) 293 return closureRedirector(root), nil 294 } 295 296 path := filepath.Join("third_party", "closure", "lib", "closure") 297 return makeFileServer(root, path, filepath.Join("goog", "base.js")) 298 } 299 300 func makeFileServer(sourceRoot string, pathToServe string, expectedContentPath string) (http.Handler, error) { 301 fi, err := os.Stat(sourceRoot) 302 if err != nil { 303 return nil, err 304 } 305 if !fi.IsDir() { 306 return nil, errors.New("not a directory") 307 } 308 dirToServe := filepath.Join(sourceRoot, pathToServe) 309 _, err = os.Stat(filepath.Join(dirToServe, expectedContentPath)) 310 if err != nil { 311 return nil, fmt.Errorf("directory doesn't contain %s; wrong directory?", expectedContentPath) 312 } 313 return http.FileServer(http.Dir(dirToServe)), nil 314 } 315 316 const closureBaseURL closureRedirector = "https://closure-library.googlecode.com/git" 317 318 // closureRedirector is a hack to redirect requests for Closure's million *.js files 319 // to https://closure-library.googlecode.com/git. 320 // TODO: this doesn't work when offline. We need to run genjsdeps over all of the Camlistore 321 // UI to figure out which Closure *.js files to fileembed and generate zembed. Then this 322 // type can be deleted. 323 type closureRedirector string 324 325 func (base closureRedirector) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 326 newURL := string(base) + "/" + path.Clean(httputil.PathSuffix(req)) 327 http.Redirect(rw, req, newURL, http.StatusTemporaryRedirect) 328 } 329 330 func camliMode(req *http.Request) string { 331 return req.URL.Query().Get("camli.mode") 332 } 333 334 func wantsDiscovery(req *http.Request) bool { 335 return httputil.IsGet(req) && 336 (req.Header.Get("Accept") == "text/x-camli-configuration" || 337 camliMode(req) == "config") 338 } 339 340 func wantsUploadHelper(req *http.Request) bool { 341 return req.Method == "POST" && camliMode(req) == "uploadhelper" 342 } 343 344 func wantsPermanode(req *http.Request) bool { 345 if httputil.IsGet(req) && blob.ValidRefString(req.FormValue("p")) { 346 // The new UI is handled by index.html. 347 if req.FormValue("newui") != "1" { 348 return true 349 } 350 } 351 return false 352 } 353 354 func wantsBlobInfo(req *http.Request) bool { 355 return httputil.IsGet(req) && blob.ValidRefString(req.FormValue("b")) 356 } 357 358 func wantsFileTreePage(req *http.Request) bool { 359 return httputil.IsGet(req) && blob.ValidRefString(req.FormValue("d")) 360 } 361 362 func getSuffixMatches(req *http.Request, pattern *regexp.Regexp) bool { 363 if httputil.IsGet(req) { 364 suffix := httputil.PathSuffix(req) 365 return pattern.MatchString(suffix) 366 } 367 return false 368 } 369 370 func (ui *UIHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 371 suffix := httputil.PathSuffix(req) 372 373 rw.Header().Set("Vary", "Accept") 374 switch { 375 case wantsDiscovery(req): 376 ui.root.serveDiscovery(rw, req) 377 case wantsUploadHelper(req): 378 ui.serveUploadHelper(rw, req) 379 case strings.HasPrefix(suffix, "download/"): 380 ui.serveDownload(rw, req) 381 case strings.HasPrefix(suffix, "thumbnail/"): 382 ui.serveThumbnail(rw, req) 383 case strings.HasPrefix(suffix, "tree/"): 384 ui.serveFileTree(rw, req) 385 case strings.HasPrefix(suffix, "qr/"): 386 ui.serveQR(rw, req) 387 case getSuffixMatches(req, closurePattern): 388 ui.serveClosure(rw, req) 389 case getSuffixMatches(req, reactPattern): 390 ui.serveFromDiskOrStatic(rw, req, reactPattern, ui.fileReactHandler, reactstatic.Files) 391 case getSuffixMatches(req, glitchPattern): 392 ui.serveFromDiskOrStatic(rw, req, glitchPattern, ui.fileGlitchHandler, glitchstatic.Files) 393 case getSuffixMatches(req, fontawesomePattern): 394 ui.serveFromDiskOrStatic(rw, req, fontawesomePattern, ui.fileFontawesomeHandler, fontawesomestatic.Files) 395 default: 396 file := "" 397 if m := staticFilePattern.FindStringSubmatch(suffix); m != nil { 398 file = m[1] 399 } else { 400 switch { 401 case wantsPermanode(req): 402 file = "permanode.html" 403 case wantsBlobInfo(req): 404 file = "blobinfo.html" 405 case wantsFileTreePage(req): 406 file = "filetree.html" 407 case req.URL.Path == httputil.PathBase(req): 408 file = "index.html" 409 default: 410 http.Error(rw, "Illegal URL.", http.StatusNotFound) 411 return 412 } 413 } 414 if file == "deps.js" { 415 serveDepsJS(rw, req, ui.uiDir) 416 return 417 } 418 serveStaticFile(rw, req, uistatic.Files, file) 419 } 420 } 421 422 func serveStaticFile(rw http.ResponseWriter, req *http.Request, root http.FileSystem, file string) { 423 f, err := root.Open("/" + file) 424 if err != nil { 425 http.NotFound(rw, req) 426 log.Printf("Failed to open file %q from embedded resources: %v", file, err) 427 return 428 } 429 defer f.Close() 430 var modTime time.Time 431 if fi, err := f.Stat(); err == nil { 432 modTime = fi.ModTime() 433 } 434 // TODO(wathiede): should pkg/magic be leveraged here somehow? It has a 435 // slightly different purpose. 436 if strings.HasSuffix(file, ".svg") { 437 rw.Header().Set("Content-Type", "image/svg+xml") 438 } 439 http.ServeContent(rw, req, file, modTime, f) 440 } 441 442 func (ui *UIHandler) populateDiscoveryMap(m map[string]interface{}) { 443 pubRoots := map[string]interface{}{} 444 for key, pubh := range ui.publishRoots { 445 m := map[string]interface{}{ 446 "name": pubh.RootName, 447 "prefix": []string{key}, 448 // TODO: include gpg key id 449 } 450 if sh, ok := ui.root.SearchHandler(); ok { 451 pn, err := sh.Index().PermanodeOfSignerAttrValue(sh.Owner(), "camliRoot", pubh.RootName) 452 if err == nil { 453 m["currentPermanode"] = pn.String() 454 } 455 } 456 pubRoots[pubh.RootName] = m 457 } 458 459 uiDisco := map[string]interface{}{ 460 "jsonSignRoot": ui.JSONSignRoot, 461 "uiRoot": ui.prefix, 462 "uploadHelper": ui.prefix + "?camli.mode=uploadhelper", // hack; remove with better javascript 463 "downloadHelper": path.Join(ui.prefix, "download") + "/", 464 "directoryHelper": path.Join(ui.prefix, "tree") + "/", 465 "publishRoots": pubRoots, 466 } 467 // TODO(mpl): decouple discovery of the sig handler from the 468 // existence of a ui handler. 469 if ui.sigh != nil { 470 uiDisco["signing"] = ui.sigh.DiscoveryMap(ui.JSONSignRoot) 471 } 472 for k, v := range uiDisco { 473 if _, ok := m[k]; ok { 474 log.Fatalf("Duplicate discovery key %q", k) 475 } 476 m[k] = v 477 } 478 } 479 480 func (ui *UIHandler) serveDownload(rw http.ResponseWriter, req *http.Request) { 481 if ui.root.Storage == nil { 482 http.Error(rw, "No BlobRoot configured", 500) 483 return 484 } 485 486 suffix := httputil.PathSuffix(req) 487 m := downloadPattern.FindStringSubmatch(suffix) 488 if m == nil { 489 httputil.ErrorRouting(rw, req) 490 return 491 } 492 493 fbr, ok := blob.Parse(m[1]) 494 if !ok { 495 http.Error(rw, "Invalid blobref", 400) 496 return 497 } 498 499 dh := &DownloadHandler{ 500 Fetcher: ui.root.Storage, 501 Cache: ui.Cache, 502 } 503 dh.ServeHTTP(rw, req, fbr) 504 } 505 506 func (ui *UIHandler) serveThumbnail(rw http.ResponseWriter, req *http.Request) { 507 if ui.root.Storage == nil { 508 http.Error(rw, "No BlobRoot configured", 500) 509 return 510 } 511 512 suffix := httputil.PathSuffix(req) 513 m := thumbnailPattern.FindStringSubmatch(suffix) 514 if m == nil { 515 httputil.ErrorRouting(rw, req) 516 return 517 } 518 519 query := req.URL.Query() 520 width, _ := strconv.Atoi(query.Get("mw")) 521 height, _ := strconv.Atoi(query.Get("mh")) 522 blobref, ok := blob.Parse(m[1]) 523 if !ok { 524 http.Error(rw, "Invalid blobref", 400) 525 return 526 } 527 528 if width == 0 { 529 width = search.MaxImageSize 530 } 531 if height == 0 { 532 height = search.MaxImageSize 533 } 534 535 th := &ImageHandler{ 536 Fetcher: ui.root.Storage, 537 Cache: ui.Cache, 538 MaxWidth: width, 539 MaxHeight: height, 540 thumbMeta: ui.thumbMeta, 541 resizeSem: ui.resizeSem, 542 } 543 th.ServeHTTP(rw, req, blobref) 544 } 545 546 func (ui *UIHandler) serveFileTree(rw http.ResponseWriter, req *http.Request) { 547 if ui.root.Storage == nil { 548 http.Error(rw, "No BlobRoot configured", 500) 549 return 550 } 551 552 suffix := httputil.PathSuffix(req) 553 m := treePattern.FindStringSubmatch(suffix) 554 if m == nil { 555 httputil.ErrorRouting(rw, req) 556 return 557 } 558 559 blobref, ok := blob.Parse(m[1]) 560 if !ok { 561 http.Error(rw, "Invalid blobref", 400) 562 return 563 } 564 565 fth := &FileTreeHandler{ 566 Fetcher: ui.root.Storage, 567 file: blobref, 568 } 569 fth.ServeHTTP(rw, req) 570 } 571 572 func (ui *UIHandler) serveClosure(rw http.ResponseWriter, req *http.Request) { 573 suffix := httputil.PathSuffix(req) 574 if ui.closureHandler == nil { 575 log.Printf("%v not served: closure handler is nil", suffix) 576 http.NotFound(rw, req) 577 return 578 } 579 m := closurePattern.FindStringSubmatch(suffix) 580 if m == nil { 581 httputil.ErrorRouting(rw, req) 582 return 583 } 584 req.URL.Path = "/" + m[1] 585 ui.closureHandler.ServeHTTP(rw, req) 586 } 587 588 // serveFromDiskOrStatic matches rx against req's path and serves the match either from disk (if non-nil) or from static (embedded in the binary). 589 func (ui *UIHandler) serveFromDiskOrStatic(rw http.ResponseWriter, req *http.Request, rx *regexp.Regexp, disk http.Handler, static *fileembed.Files) { 590 suffix := httputil.PathSuffix(req) 591 m := rx.FindStringSubmatch(suffix) 592 if m == nil { 593 panic("Caller should verify that rx matches") 594 } 595 file := m[1] 596 if disk != nil { 597 req.URL.Path = "/" + file 598 disk.ServeHTTP(rw, req) 599 } else { 600 serveStaticFile(rw, req, static, file) 601 } 602 603 } 604 605 func (ui *UIHandler) serveQR(rw http.ResponseWriter, req *http.Request) { 606 url := req.URL.Query().Get("url") 607 if url == "" { 608 http.Error(rw, "Missing url parameter.", http.StatusBadRequest) 609 return 610 } 611 code, err := qr.Encode(url, qr.L) 612 if err != nil { 613 http.Error(rw, err.Error(), http.StatusInternalServerError) 614 return 615 } 616 rw.Header().Set("Content-Type", "image/png") 617 rw.Write(code.PNG()) 618 } 619 620 // serveDepsJS serves an auto-generated Closure deps.js file. 621 func serveDepsJS(rw http.ResponseWriter, req *http.Request, dir string) { 622 var root http.FileSystem 623 if dir == "" { 624 root = uistatic.Files 625 } else { 626 root = http.Dir(dir) 627 } 628 629 b, err := closure.GenDeps(root) 630 if err != nil { 631 log.Print(err) 632 http.Error(rw, "Server error", 500) 633 return 634 } 635 rw.Header().Set("Content-Type", "text/javascript; charset=utf-8") 636 rw.Write([]byte("// auto-generated from camlistored\n")) 637 rw.Write(b) 638 }