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  }