2011-04-16 22:44:22 +00:00
|
|
|
/*
|
|
|
|
Copyright 2011 Google Inc.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2011-10-18 18:12:01 +00:00
|
|
|
package server
|
2011-04-16 22:44:22 +00:00
|
|
|
|
|
|
|
import (
|
2012-11-07 17:57:43 +00:00
|
|
|
"errors"
|
2011-04-16 22:44:22 +00:00
|
|
|
"fmt"
|
2011-05-26 04:56:48 +00:00
|
|
|
"log"
|
Update from r60 to [almost] Go 1.
A lot is still broken, but most stuff at least compiles now.
The directory tree has been rearranged now too. Go libraries are now
under "pkg". Fully qualified, they are e.g. "camlistore.org/pkg/jsonsign".
The go tool cannot yet fetch from arbitrary domains, but discussion is
happening now on which mechanism to use to allow that.
For now, put the camlistore root under $GOPATH/src. Typically $GOPATH
is $HOME, so Camlistore should be at $HOME/src/camlistore.org.
Then you can:
$ go build ./server/camlistored
... etc
The build.pl script is currently disabled. It'll be resurrected at
some point, but with a very different role (helping create a fake
GOPATH and running the go build command, if things are installed at
the wrong place, and/or running fileembed generators).
Many things are certainly broken.
Many things are disabled. (MySQL, all indexing, etc).
Many things need to be moved into
camlistore.org/third_party/{code.google.com,github.com} and updated
from their r60 to Go 1 versions, where applicable.
The GoMySQL stuff should be updated to use database/sql and the ziutek
library implementing database/sql/driver.
Help wanted.
Change-Id: If71217dc5c8f0e70dbe46e9504ca5131c6eeacde
2012-02-19 05:53:06 +00:00
|
|
|
"net/http"
|
2012-12-23 20:39:15 +00:00
|
|
|
"os"
|
2012-05-13 20:02:20 +00:00
|
|
|
"path"
|
2012-12-03 22:46:08 +00:00
|
|
|
"path/filepath"
|
2011-04-16 22:44:22 +00:00
|
|
|
"regexp"
|
2011-06-15 08:42:18 +00:00
|
|
|
"strconv"
|
2011-05-31 17:20:28 +00:00
|
|
|
"strings"
|
2012-12-12 02:29:58 +00:00
|
|
|
"time"
|
2011-04-16 22:44:22 +00:00
|
|
|
|
2016-04-20 23:43:47 +00:00
|
|
|
fontawesomestatic "embed/fontawesome"
|
|
|
|
glitchstatic "embed/glitch"
|
server/camlistored/ui: improve map aspect search and markers
Notably:
-do not load any markers on an empty search query, because that would
mean loading absolutely all of the items with a location, which seems
like a bad idea.
-use different markers for different nodes. For now, foursquare
checkins, file images, and files have their own marker.
-vendor in https://github.com/lvoogdt/Leaflet.awesome-markers to achieve
the above, which relies on Font Awesome, which we already have in.
icons available for the markers: http://fontawesome.io/icons/
-when no location can be inferred from the search query, set the view to
encompass all markers that were drawn.
-when a location search is known, draw a rectangle representing the
results zone.
-use thumber for image in marker popup
-use title, if possible, instead of blobRef for link text in marker
popup
-switch to directly using OpenStreetMap tiles, instead of MapBox ones.
https://storage.googleapis.com/camlistore-screenshots/Screenshot_20170622-232359.png
Change-Id: Ibc84fa988aea8b8d3a2588ee8790adf6d9b5ad7a
2017-06-21 16:36:15 +00:00
|
|
|
leafletstatic "embed/leaflet"
|
2016-04-20 23:43:47 +00:00
|
|
|
lessstatic "embed/less"
|
2016-06-06 23:53:46 +00:00
|
|
|
opensansstatic "embed/opensans"
|
2016-04-20 23:43:47 +00:00
|
|
|
reactstatic "embed/react"
|
|
|
|
|
2015-12-01 16:19:49 +00:00
|
|
|
"go4.org/jsonconfig"
|
2015-11-20 22:27:00 +00:00
|
|
|
"go4.org/syncutil"
|
Rename import paths from camlistore.org to perkeep.org.
Part of the project renaming, issue #981.
After this, users will need to mv their $GOPATH/src/camlistore.org to
$GOPATH/src/perkeep.org. Sorry.
This doesn't yet rename the tools like camlistored, camput, camget,
camtool, etc.
Also, this only moves the lru package to internal. More will move to
internal later.
Also, this doesn't yet remove the "/pkg/" directory. That'll likely
happen later.
This updates some docs, but not all.
devcam test now passes again, even with Go 1.10 (which requires vet
checks are clean too). So a bunch of vet tests are fixed in this CL
too, and a bunch of other broken tests are now fixed (introduced from
the past week of merging the CL backlog).
Change-Id: If580db1691b5b99f8ed6195070789b1f44877dd4
2018-01-01 22:41:41 +00:00
|
|
|
"perkeep.org/pkg/blob"
|
|
|
|
"perkeep.org/pkg/blobserver"
|
|
|
|
"perkeep.org/pkg/cacher"
|
|
|
|
"perkeep.org/pkg/constants"
|
|
|
|
"perkeep.org/pkg/fileembed"
|
|
|
|
"perkeep.org/pkg/httputil"
|
|
|
|
"perkeep.org/pkg/misc/closure"
|
|
|
|
"perkeep.org/pkg/osutil"
|
|
|
|
"perkeep.org/pkg/search"
|
|
|
|
"perkeep.org/pkg/server/app"
|
|
|
|
"perkeep.org/pkg/sorted"
|
|
|
|
"perkeep.org/pkg/types/camtypes"
|
|
|
|
uistatic "perkeep.org/server/camlistored/ui"
|
|
|
|
closurestatic "perkeep.org/server/camlistored/ui/closure"
|
2017-11-20 20:08:08 +00:00
|
|
|
"rsc.io/qr"
|
2011-04-16 22:44:22 +00:00
|
|
|
)
|
|
|
|
|
2012-05-13 19:14:31 +00:00
|
|
|
var (
|
pkg/server: add files "zipper" to DownloadHandler
We want to add a feature for clients (the web UI), where they can select
a bunch of files and ask the server for a zip archive of all these files.
This CL modifies the DownloadHandler so it does exactly that upon
reception of a POST request with a query parameter of the form
files=sha1-foo,sha1-bar,sha1-baz
This CL also adds a new button to the contextual sidebar of the web UI,
that takes care of sending the download request to the server.
known limitations: only permanodes with file as camliContent are
accepted as a valid selection (i.e. no sets, or static-dirs, etc) for
now.
Implementation detail:
We're creating an ephemeral DOM form on the fly to send the request.
The reason is: if we sent it as a Go http request, we'd have to read
the response manually and then we'd have no way of writing it to disk.
If we did it with an xhr, we could write the response to disk by
creating a File or Blob and then using URL.createObjectURL(), but we'd
have to keep the response in memory while doing so, which is
unacceptable for large enough archives.
Fixes #899
Change-Id: I104f7c5bd10ab3369e28d33752380dd12b5b3e6b
2017-03-06 20:02:19 +00:00
|
|
|
staticFilePattern = regexp.MustCompile(`^([a-zA-Z0-9\-\_\.]+\.(html|js|css|png|jpg|gif|svg))$`)
|
|
|
|
identOrDotPattern = regexp.MustCompile(`^[a-zA-Z\_]+(\.[a-zA-Z\_]+)*$`)
|
2014-04-05 05:59:43 +00:00
|
|
|
thumbnailPattern = regexp.MustCompile(`^thumbnail/([^/]+)(/.*)?$`)
|
|
|
|
treePattern = regexp.MustCompile(`^tree/([^/]+)(/.*)?$`)
|
|
|
|
closurePattern = regexp.MustCompile(`^closure/(([^/]+)(/.*)?)$`)
|
2014-09-04 15:30:59 +00:00
|
|
|
lessPattern = regexp.MustCompile(`^less/(.+)$`)
|
2014-04-05 05:59:43 +00:00
|
|
|
reactPattern = regexp.MustCompile(`^react/(.+)$`)
|
server/camlistored/ui: improve map aspect search and markers
Notably:
-do not load any markers on an empty search query, because that would
mean loading absolutely all of the items with a location, which seems
like a bad idea.
-use different markers for different nodes. For now, foursquare
checkins, file images, and files have their own marker.
-vendor in https://github.com/lvoogdt/Leaflet.awesome-markers to achieve
the above, which relies on Font Awesome, which we already have in.
icons available for the markers: http://fontawesome.io/icons/
-when no location can be inferred from the search query, set the view to
encompass all markers that were drawn.
-when a location search is known, draw a rectangle representing the
results zone.
-use thumber for image in marker popup
-use title, if possible, instead of blobRef for link text in marker
popup
-switch to directly using OpenStreetMap tiles, instead of MapBox ones.
https://storage.googleapis.com/camlistore-screenshots/Screenshot_20170622-232359.png
Change-Id: Ibc84fa988aea8b8d3a2588ee8790adf6d9b5ad7a
2017-06-21 16:36:15 +00:00
|
|
|
leafletPattern = regexp.MustCompile(`^leaflet/(.+)$`)
|
2014-04-05 05:59:43 +00:00
|
|
|
fontawesomePattern = regexp.MustCompile(`^fontawesome/(.+)$`)
|
2016-06-06 23:53:46 +00:00
|
|
|
openSansPattern = regexp.MustCompile(`^opensans/(([^/]+)(/.*)?)$`)
|
2014-04-05 05:59:43 +00:00
|
|
|
glitchPattern = regexp.MustCompile(`^glitch/(.+)$`)
|
2013-12-17 02:11:52 +00:00
|
|
|
|
|
|
|
disableThumbCache, _ = strconv.ParseBool(os.Getenv("CAMLI_DISABLE_THUMB_CACHE"))
|
2016-04-21 19:08:14 +00:00
|
|
|
|
|
|
|
vendorEmbed = filepath.Join("vendor", "embed")
|
2012-05-13 19:14:31 +00:00
|
|
|
)
|
2011-04-16 22:44:22 +00:00
|
|
|
|
|
|
|
// UIHandler handles serving the UI and discovery JSON.
|
|
|
|
type UIHandler struct {
|
2014-06-14 20:14:34 +00:00
|
|
|
publishRoots map[string]*publishRoot
|
2011-06-19 20:09:43 +00:00
|
|
|
|
2012-11-07 17:57:43 +00:00
|
|
|
prefix string // of the UI handler itself
|
|
|
|
root *RootHandler
|
2015-02-05 05:07:59 +00:00
|
|
|
search *search.Handler
|
2012-11-07 17:57:43 +00:00
|
|
|
|
2013-12-14 14:16:21 +00:00
|
|
|
// Cache optionally specifies a cache blob server, used for
|
|
|
|
// caching image thumbnails and other emphemeral data.
|
2012-11-07 17:57:43 +00:00
|
|
|
Cache blobserver.Storage // or nil
|
2013-12-14 14:16:21 +00:00
|
|
|
|
2014-01-06 01:49:59 +00:00
|
|
|
// Limit peak RAM used by concurrent image thumbnail calls.
|
|
|
|
resizeSem *syncutil.Sem
|
2014-06-14 20:14:34 +00:00
|
|
|
thumbMeta *ThumbMeta // optional thumbnail key->blob.Ref cache
|
2011-07-05 19:10:35 +00:00
|
|
|
|
2013-06-19 06:14:36 +00:00
|
|
|
// sourceRoot optionally specifies the path to root of Camlistore's
|
2013-06-12 17:10:24 +00:00
|
|
|
// source. If empty, the UI files must be compiled in to the
|
2013-06-19 06:14:36 +00:00
|
|
|
// binary (with go run make.go). This comes from the "sourceRoot"
|
2013-06-12 17:10:24 +00:00
|
|
|
// ui handler config option.
|
2013-06-19 06:14:36 +00:00
|
|
|
sourceRoot string
|
2013-06-12 17:10:24 +00:00
|
|
|
|
2013-06-20 21:58:12 +00:00
|
|
|
uiDir string // if sourceRoot != "", this is sourceRoot+"/server/camlistored/ui"
|
|
|
|
|
2014-04-05 05:59:43 +00:00
|
|
|
closureHandler http.Handler
|
2014-09-04 15:30:59 +00:00
|
|
|
fileLessHandler http.Handler
|
2014-04-05 05:59:43 +00:00
|
|
|
fileReactHandler http.Handler
|
server/camlistored/ui: improve map aspect search and markers
Notably:
-do not load any markers on an empty search query, because that would
mean loading absolutely all of the items with a location, which seems
like a bad idea.
-use different markers for different nodes. For now, foursquare
checkins, file images, and files have their own marker.
-vendor in https://github.com/lvoogdt/Leaflet.awesome-markers to achieve
the above, which relies on Font Awesome, which we already have in.
icons available for the markers: http://fontawesome.io/icons/
-when no location can be inferred from the search query, set the view to
encompass all markers that were drawn.
-when a location search is known, draw a rectangle representing the
results zone.
-use thumber for image in marker popup
-use title, if possible, instead of blobRef for link text in marker
popup
-switch to directly using OpenStreetMap tiles, instead of MapBox ones.
https://storage.googleapis.com/camlistore-screenshots/Screenshot_20170622-232359.png
Change-Id: Ibc84fa988aea8b8d3a2588ee8790adf6d9b5ad7a
2017-06-21 16:36:15 +00:00
|
|
|
fileLeafletHandler http.Handler
|
2014-04-05 05:59:43 +00:00
|
|
|
fileFontawesomeHandler http.Handler
|
2016-06-06 23:53:46 +00:00
|
|
|
fileOpenSansHandler http.Handler
|
2014-04-05 05:59:43 +00:00
|
|
|
fileGlitchHandler http.Handler
|
2011-04-16 22:44:22 +00:00
|
|
|
}
|
|
|
|
|
2011-05-26 14:34:39 +00:00
|
|
|
func init() {
|
2013-06-12 15:49:35 +00:00
|
|
|
blobserver.RegisterHandlerConstructor("ui", uiFromConfig)
|
2011-05-26 14:34:39 +00:00
|
|
|
}
|
|
|
|
|
2013-12-14 17:37:56 +00:00
|
|
|
// newKVOrNil wraps sorted.NewKeyValue and adds the ability
|
|
|
|
// to pass a nil conf to get a (nil, nil) response.
|
|
|
|
func newKVOrNil(conf jsonconfig.Obj) (sorted.KeyValue, error) {
|
|
|
|
if len(conf) == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
pkg/sorted/mysql: drop tables on reindex
When reindexing on a (My)SQL based sorted.KeyValue, we should recreate
the database schema from scratch, which means dropping the tables.
However, index.Reindex just calls Wipe on the newly created
sorted.KeyValue, which only deletes the rows, and does not drop the
tables.
Therefore, this CL changes the implementation of Wipe in the MySQL case,
so that it takes care of dropping the tables, and doing everything that
needs to be done afterwards to set up the sorted.KeyValue.
In addition, with the introduction of the sorted.NeedWipeError, we detect
upon initialization of a sorted.KeyValue if it failed because it needed
a schema update. If that is the case, and we're in reindex mode, we can
fix the sorted.KeyValue with a Wipe and carry on.
Finally, we introduce the new sorted.NewKeyValueMaybeWipe function that
automatically wipes a KeyValue when a NeedWipeError was returned upon
its creation.
Next, do the same with other sorted SQLs.
Fixes #806
Change-Id: I2032781cbf453a364880bd3e2e8b3c09aac7aed9
2016-08-16 21:41:17 +00:00
|
|
|
return sorted.NewKeyValueMaybeWipe(conf)
|
2013-12-14 17:37:56 +00:00
|
|
|
}
|
|
|
|
|
2013-06-12 15:49:35 +00:00
|
|
|
func uiFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handler, err error) {
|
2012-05-13 19:20:18 +00:00
|
|
|
ui := &UIHandler{
|
2015-05-02 12:26:33 +00:00
|
|
|
prefix: ld.MyPrefix(),
|
|
|
|
sourceRoot: conf.OptionalString("sourceRoot", ""),
|
2014-01-06 01:49:59 +00:00
|
|
|
resizeSem: syncutil.NewSem(int64(conf.OptionalInt("maxResizeBytes",
|
2014-01-06 16:08:47 +00:00
|
|
|
constants.DefaultMaxResizeMem))),
|
2012-05-13 19:20:18 +00:00
|
|
|
}
|
2011-06-04 16:58:50 +00:00
|
|
|
cachePrefix := conf.OptionalString("cache", "")
|
2013-12-14 17:37:56 +00:00
|
|
|
scaledImageConf := conf.OptionalObject("scaledImage")
|
2011-04-16 22:44:22 +00:00
|
|
|
if err = conf.Validate(); err != nil {
|
|
|
|
return
|
|
|
|
}
|
2011-05-09 21:20:19 +00:00
|
|
|
|
2013-12-14 17:37:56 +00:00
|
|
|
scaledImageKV, err := newKVOrNil(scaledImageConf)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("in UI handler's scaledImage: %v", err)
|
|
|
|
}
|
|
|
|
if scaledImageKV != nil && cachePrefix == "" {
|
|
|
|
return nil, fmt.Errorf("in UI handler, can't specify scaledImage without cache")
|
|
|
|
}
|
2011-06-04 16:58:50 +00:00
|
|
|
if cachePrefix != "" {
|
|
|
|
bs, err := ld.GetStorage(cachePrefix)
|
|
|
|
if err != nil {
|
2011-06-17 03:45:47 +00:00
|
|
|
return nil, fmt.Errorf("UI handler's cache of %q error: %v", cachePrefix, err)
|
2011-06-04 16:58:50 +00:00
|
|
|
}
|
|
|
|
ui.Cache = bs
|
2014-06-14 20:14:34 +00:00
|
|
|
ui.thumbMeta = NewThumbMeta(scaledImageKV)
|
2011-06-04 16:58:50 +00:00
|
|
|
}
|
|
|
|
|
2013-06-20 21:58:12 +00:00
|
|
|
if ui.sourceRoot == "" {
|
|
|
|
ui.sourceRoot = os.Getenv("CAMLI_DEV_CAMLI_ROOT")
|
2013-08-16 16:25:14 +00:00
|
|
|
if uistatic.IsAppEngine {
|
|
|
|
if _, err = os.Stat(filepath.Join(uistatic.GaeSourceRoot,
|
|
|
|
filepath.FromSlash("server/camlistored/ui/index.html"))); err != nil {
|
|
|
|
hint := fmt.Sprintf("\"sourceRoot\" was not specified in the config,"+
|
|
|
|
" and the default sourceRoot dir %v does not exist or does not contain"+
|
2013-08-19 17:33:53 +00:00
|
|
|
" \"server/camlistored/ui/index.html\". devcam appengine can do that for you.",
|
2013-08-16 16:25:14 +00:00
|
|
|
uistatic.GaeSourceRoot)
|
|
|
|
log.Print(hint)
|
2017-12-10 09:13:00 +00:00
|
|
|
return nil, errors.New("no sourceRoot found; UI not available")
|
2013-08-16 16:25:14 +00:00
|
|
|
}
|
|
|
|
log.Printf("Using the default \"%v\" as the sourceRoot for AppEngine", uistatic.GaeSourceRoot)
|
|
|
|
ui.sourceRoot = uistatic.GaeSourceRoot
|
|
|
|
}
|
2016-05-06 23:05:22 +00:00
|
|
|
if ui.sourceRoot == "" && uistatic.Files.IsEmpty() {
|
|
|
|
ui.sourceRoot, err = osutil.GoPackagePath("camlistore.org")
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Warning: server not compiled with linked-in UI resources (HTML, JS, CSS), and camlistore.org not found in GOPATH.")
|
|
|
|
} else {
|
|
|
|
log.Printf("Using UI resources (HTML, JS, CSS) from disk, under %v", ui.sourceRoot)
|
|
|
|
}
|
|
|
|
}
|
2013-06-20 21:58:12 +00:00
|
|
|
}
|
|
|
|
if ui.sourceRoot != "" {
|
|
|
|
ui.uiDir = filepath.Join(ui.sourceRoot, filepath.FromSlash("server/camlistored/ui"))
|
|
|
|
// Ignore any fileembed files:
|
|
|
|
Files = &fileembed.Files{
|
|
|
|
DirFallback: filepath.Join(ui.sourceRoot, filepath.FromSlash("pkg/server")),
|
|
|
|
}
|
|
|
|
uistatic.Files = &fileembed.Files{
|
|
|
|
DirFallback: ui.uiDir,
|
2013-07-04 23:07:50 +00:00
|
|
|
Listable: true,
|
2013-08-16 16:25:14 +00:00
|
|
|
// In dev_appserver, allow edit-and-reload without
|
|
|
|
// restarting. In production, though, it's faster to just
|
|
|
|
// slurp it in.
|
|
|
|
SlurpToMemory: uistatic.IsProdAppEngine,
|
2013-06-20 21:58:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-06-19 06:14:36 +00:00
|
|
|
ui.closureHandler, err = ui.makeClosureHandler(ui.sourceRoot)
|
2013-06-12 17:10:24 +00:00
|
|
|
if err != nil {
|
2013-06-19 06:14:36 +00:00
|
|
|
return nil, fmt.Errorf(`Invalid "sourceRoot" value of %q: %v"`, ui.sourceRoot, err)
|
2012-12-12 17:04:05 +00:00
|
|
|
}
|
2011-07-05 19:10:35 +00:00
|
|
|
|
2013-12-29 05:27:28 +00:00
|
|
|
if ui.sourceRoot != "" {
|
2017-01-25 17:19:51 +00:00
|
|
|
ui.fileReactHandler, err = makeFileServer(ui.sourceRoot, filepath.Join(vendorEmbed, "react"), "react-dom.min.js")
|
2013-12-29 05:27:28 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Could not make react handler: %s", err)
|
|
|
|
}
|
server/camlistored/ui: improve map aspect search and markers
Notably:
-do not load any markers on an empty search query, because that would
mean loading absolutely all of the items with a location, which seems
like a bad idea.
-use different markers for different nodes. For now, foursquare
checkins, file images, and files have their own marker.
-vendor in https://github.com/lvoogdt/Leaflet.awesome-markers to achieve
the above, which relies on Font Awesome, which we already have in.
icons available for the markers: http://fontawesome.io/icons/
-when no location can be inferred from the search query, set the view to
encompass all markers that were drawn.
-when a location search is known, draw a rectangle representing the
results zone.
-use thumber for image in marker popup
-use title, if possible, instead of blobRef for link text in marker
popup
-switch to directly using OpenStreetMap tiles, instead of MapBox ones.
https://storage.googleapis.com/camlistore-screenshots/Screenshot_20170622-232359.png
Change-Id: Ibc84fa988aea8b8d3a2588ee8790adf6d9b5ad7a
2017-06-21 16:36:15 +00:00
|
|
|
ui.fileLeafletHandler, err = makeFileServer(ui.sourceRoot, filepath.Join(vendorEmbed, "leaflet"), "leaflet.js")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Could not make leaflet handler: %s", err)
|
|
|
|
}
|
2016-04-21 19:08:14 +00:00
|
|
|
ui.fileGlitchHandler, err = makeFileServer(ui.sourceRoot, filepath.Join(vendorEmbed, "glitch"), "npc_piggy__x1_walk_png_1354829432.png")
|
2013-12-31 23:25:19 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Could not make glitch handler: %s", err)
|
|
|
|
}
|
2016-04-21 19:08:14 +00:00
|
|
|
ui.fileFontawesomeHandler, err = makeFileServer(ui.sourceRoot, filepath.Join(vendorEmbed, "fontawesome"), "css/font-awesome.css")
|
2014-04-05 05:59:43 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Could not make fontawesome handler: %s", err)
|
|
|
|
}
|
2016-04-21 19:08:14 +00:00
|
|
|
ui.fileLessHandler, err = makeFileServer(ui.sourceRoot, filepath.Join(vendorEmbed, "less"), "less.js")
|
2014-09-05 20:34:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Could not make less handler: %s", err)
|
|
|
|
}
|
2016-06-06 23:53:46 +00:00
|
|
|
ui.fileOpenSansHandler, err = makeFileServer(ui.sourceRoot, filepath.Join(vendorEmbed, "opensans"), "OpenSans.css")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Could not make Open Sans handler: %s", err)
|
|
|
|
}
|
2013-12-29 05:27:28 +00:00
|
|
|
}
|
|
|
|
|
2012-11-07 17:57:43 +00:00
|
|
|
rootPrefix, _, err := ld.FindHandlerByType("root")
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.New("No root handler configured, which is necessary for the ui handler")
|
|
|
|
}
|
|
|
|
if h, err := ld.GetHandler(rootPrefix); err == nil {
|
|
|
|
ui.root = h.(*RootHandler)
|
|
|
|
ui.root.registerUIHandler(ui)
|
|
|
|
} else {
|
|
|
|
return nil, errors.New("failed to find the 'root' handler")
|
|
|
|
}
|
|
|
|
|
2011-04-16 22:44:22 +00:00
|
|
|
return ui, nil
|
|
|
|
}
|
|
|
|
|
2014-06-14 20:14:34 +00:00
|
|
|
type publishRoot struct {
|
|
|
|
Name string
|
|
|
|
Permanode blob.Ref
|
|
|
|
Prefix string
|
|
|
|
}
|
|
|
|
|
|
|
|
// InitHandler goes through all the other configured handlers to discover
|
|
|
|
// the publisher ones, and uses them to populate ui.publishRoots.
|
|
|
|
func (ui *UIHandler) InitHandler(hl blobserver.FindHandlerByTyper) error {
|
2014-06-16 23:03:01 +00:00
|
|
|
// InitHandler is called after all handlers have been setup, so the bootstrap
|
|
|
|
// of the camliRoot node for publishers in dev-mode is already done.
|
2014-06-14 20:14:34 +00:00
|
|
|
searchPrefix, _, err := hl.FindHandlerByType("search")
|
|
|
|
if err != nil {
|
|
|
|
return errors.New("No search handler configured, which is necessary for the ui handler")
|
|
|
|
}
|
|
|
|
var sh *search.Handler
|
|
|
|
htype, hi := hl.AllHandlers()
|
|
|
|
if h, ok := hi[searchPrefix]; !ok {
|
|
|
|
return errors.New("failed to find the \"search\" handler")
|
|
|
|
} else {
|
|
|
|
sh = h.(*search.Handler)
|
2015-02-05 05:07:59 +00:00
|
|
|
ui.search = sh
|
2014-06-14 20:14:34 +00:00
|
|
|
}
|
|
|
|
camliRootQuery := func(camliRoot string) (*search.SearchResult, error) {
|
|
|
|
return sh.Query(&search.SearchQuery{
|
|
|
|
Limit: 1,
|
|
|
|
Constraint: &search.Constraint{
|
|
|
|
Permanode: &search.PermanodeConstraint{
|
|
|
|
Attr: "camliRoot",
|
|
|
|
Value: camliRoot,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
for prefix, typ := range htype {
|
|
|
|
if typ != "app" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
ah, ok := hi[prefix].(*app.Handler)
|
|
|
|
if !ok {
|
|
|
|
panic(fmt.Sprintf("UI: handler for %v has type \"app\" but is not app.Handler", prefix))
|
|
|
|
}
|
pkg/server/app: proxy search requests for publisher
Some of the publisher features have moved from the server-side app to
the client-side app (the browser) thanks to gopherjs. Some of these
features imply doing some search queries against Camlistore, which
requires authentication. The server-side app receives the necessary
credentials on creation, from Camlistore. However, we can't just
communicate them to the client-side (as we do with the web UI) since the
publisher app itself does not require any auth and is supposed to be
exposed to the world.
Therefore, we need to allow some search queries to be done without
authentication.
To this end, the app handler on Camlistore now assumes a new role: it is
also a search proxy for the app. The app sends an unauthenticated search
query to the app handler (instead of directly to the search handler),
and it is the role of the app handler to verify that this query is
allowed for the app, and if yes, to forward the search to the Camlistore's
search handler.
We introduce a new mechanism to filter the search queries in the form of
a master query. Upon startup, the publisher registers, using the new
CAMLI_APP_MASTERQUERY_URL env var, a *search.SearchQuery with the app
handler. The app handler runs that query and caches all the blob refs
included in the response to that query. In the following, all incoming
search queries are run by the app handler, which checks that none of the
response blobs are out of the set defined by the aforementioned cached
blob refs. If that check fails, the search response is not forwarded to
the app/client.
The process can be improved in a subsequent CL (or patchset), with finer
grained domains, i.e. a master search query per published camliPath,
instead of one for the whole app handler.
Change-Id: I00d91ff73e0cbe78744bfae9878077dc3a8521f4
2016-07-20 15:54:29 +00:00
|
|
|
// TODO(mpl): this check is weak, as the user could very well
|
|
|
|
// use another binary name for the publisher app. We should
|
|
|
|
// introduce/use another identifier.
|
2014-06-14 20:14:34 +00:00
|
|
|
if ah.ProgramName() != "publisher" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
appConfig := ah.AppConfig()
|
|
|
|
if appConfig == nil {
|
|
|
|
log.Printf("UI: app handler for %v has no appConfig", prefix)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
camliRoot, ok := appConfig["camliRoot"].(string)
|
|
|
|
if !ok {
|
2014-11-12 22:21:42 +00:00
|
|
|
log.Printf("UI: camliRoot in appConfig is %T, want string", appConfig["camliRoot"])
|
2014-06-14 20:14:34 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
result, err := camliRootQuery(camliRoot)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("UI: could not find permanode for camliRoot %v: %v", camliRoot, err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if len(result.Blobs) == 0 || !result.Blobs[0].Blob.Valid() {
|
|
|
|
log.Printf("UI: no valid permanode for camliRoot %v", camliRoot)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if ui.publishRoots == nil {
|
|
|
|
ui.publishRoots = make(map[string]*publishRoot)
|
|
|
|
}
|
|
|
|
ui.publishRoots[prefix] = &publishRoot{
|
|
|
|
Name: camliRoot,
|
|
|
|
Prefix: prefix,
|
|
|
|
Permanode: result.Blobs[0].Blob,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2013-06-28 15:15:48 +00:00
|
|
|
func (ui *UIHandler) makeClosureHandler(root string) (http.Handler, error) {
|
|
|
|
return makeClosureHandler(root, "ui")
|
|
|
|
}
|
|
|
|
|
2013-06-12 17:10:24 +00:00
|
|
|
// makeClosureHandler returns a handler to serve Closure files.
|
|
|
|
// root is either:
|
2013-06-19 06:14:36 +00:00
|
|
|
// 1) empty: use the Closure files compiled in to the binary (if
|
|
|
|
// available), else redirect to the Internet.
|
|
|
|
// 2) a URL prefix: base of Camlistore to get Closure to redirect to
|
|
|
|
// 3) a path on disk to the root of camlistore's source (which
|
|
|
|
// contains the necessary subset of Closure files)
|
2013-06-28 15:15:48 +00:00
|
|
|
func makeClosureHandler(root, handlerName string) (http.Handler, error) {
|
2013-08-16 16:25:14 +00:00
|
|
|
// devcam server environment variable takes precedence:
|
2013-06-19 06:14:36 +00:00
|
|
|
if d := os.Getenv("CAMLI_DEV_CLOSURE_DIR"); d != "" {
|
2013-07-29 13:59:53 +00:00
|
|
|
log.Printf("%v: serving Closure from devcam server's $CAMLI_DEV_CLOSURE_DIR: %v", handlerName, d)
|
2013-06-19 06:14:36 +00:00
|
|
|
return http.FileServer(http.Dir(d)), nil
|
|
|
|
}
|
2013-06-12 17:10:24 +00:00
|
|
|
if root == "" {
|
2013-06-19 06:14:36 +00:00
|
|
|
fs, err := closurestatic.FileSystem()
|
|
|
|
if err == os.ErrNotExist {
|
2013-06-28 15:15:48 +00:00
|
|
|
log.Printf("%v: no configured setting or embedded resources; serving Closure via %v", handlerName, closureBaseURL)
|
2013-06-19 06:14:36 +00:00
|
|
|
return closureBaseURL, nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error loading embedded Closure zip file: %v", err)
|
|
|
|
}
|
2013-06-28 15:15:48 +00:00
|
|
|
log.Printf("%v: serving Closure from embedded resources", handlerName)
|
2013-06-19 06:14:36 +00:00
|
|
|
return http.FileServer(fs), nil
|
2013-06-12 17:10:24 +00:00
|
|
|
}
|
|
|
|
if strings.HasPrefix(root, "http") {
|
2013-06-28 15:15:48 +00:00
|
|
|
log.Printf("%v: serving Closure using redirects to %v", handlerName, root)
|
2013-06-12 17:10:24 +00:00
|
|
|
return closureRedirector(root), nil
|
|
|
|
}
|
2013-12-29 05:27:28 +00:00
|
|
|
|
2016-04-21 19:08:14 +00:00
|
|
|
path := filepath.Join(vendorEmbed, "closure", "lib", "closure")
|
2013-12-31 23:25:19 +00:00
|
|
|
return makeFileServer(root, path, filepath.Join("goog", "base.js"))
|
2013-12-29 05:27:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func makeFileServer(sourceRoot string, pathToServe string, expectedContentPath string) (http.Handler, error) {
|
|
|
|
fi, err := os.Stat(sourceRoot)
|
2013-06-12 17:10:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if !fi.IsDir() {
|
|
|
|
return nil, errors.New("not a directory")
|
|
|
|
}
|
2013-12-29 05:27:28 +00:00
|
|
|
dirToServe := filepath.Join(sourceRoot, pathToServe)
|
|
|
|
_, err = os.Stat(filepath.Join(dirToServe, expectedContentPath))
|
2013-06-12 17:10:24 +00:00
|
|
|
if err != nil {
|
2013-12-29 05:27:28 +00:00
|
|
|
return nil, fmt.Errorf("directory doesn't contain %s; wrong directory?", expectedContentPath)
|
2013-06-12 17:10:24 +00:00
|
|
|
}
|
2013-12-29 05:27:28 +00:00
|
|
|
return http.FileServer(http.Dir(dirToServe)), nil
|
2013-06-12 17:10:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const closureBaseURL closureRedirector = "https://closure-library.googlecode.com/git"
|
2013-06-11 09:50:56 +00:00
|
|
|
|
2013-06-12 09:05:01 +00:00
|
|
|
// closureRedirector is a hack to redirect requests for Closure's million *.js files
|
|
|
|
// to https://closure-library.googlecode.com/git.
|
|
|
|
// TODO: this doesn't work when offline. We need to run genjsdeps over all of the Camlistore
|
|
|
|
// UI to figure out which Closure *.js files to fileembed and generate zembed. Then this
|
|
|
|
// type can be deleted.
|
2013-06-12 17:10:24 +00:00
|
|
|
type closureRedirector string
|
2013-06-11 09:50:56 +00:00
|
|
|
|
2013-06-12 17:10:24 +00:00
|
|
|
func (base closureRedirector) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
newURL := string(base) + "/" + path.Clean(httputil.PathSuffix(req))
|
2013-06-11 09:50:56 +00:00
|
|
|
http.Redirect(rw, req, newURL, http.StatusTemporaryRedirect)
|
|
|
|
}
|
|
|
|
|
2011-05-26 04:56:48 +00:00
|
|
|
func camliMode(req *http.Request) string {
|
2012-05-13 19:13:58 +00:00
|
|
|
return req.URL.Query().Get("camli.mode")
|
2011-05-26 04:56:48 +00:00
|
|
|
}
|
|
|
|
|
2014-08-17 02:13:36 +00:00
|
|
|
func wantsBlobRef(req *http.Request) bool {
|
|
|
|
_, ok := blob.ParseKnown(httputil.PathSuffix(req))
|
|
|
|
return ok
|
|
|
|
}
|
|
|
|
|
2011-04-16 23:18:31 +00:00
|
|
|
func wantsDiscovery(req *http.Request) bool {
|
2013-09-15 19:12:26 +00:00
|
|
|
return httputil.IsGet(req) &&
|
2011-04-16 23:18:31 +00:00
|
|
|
(req.Header.Get("Accept") == "text/x-camli-configuration" ||
|
2011-05-29 17:50:17 +00:00
|
|
|
camliMode(req) == "config")
|
2011-05-26 04:56:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func wantsUploadHelper(req *http.Request) bool {
|
|
|
|
return req.Method == "POST" && camliMode(req) == "uploadhelper"
|
2011-04-16 23:18:31 +00:00
|
|
|
}
|
|
|
|
|
2011-05-30 04:39:23 +00:00
|
|
|
func wantsPermanode(req *http.Request) bool {
|
2014-08-15 06:04:37 +00:00
|
|
|
return httputil.IsGet(req) && blob.ValidRefString(req.FormValue("p"))
|
2011-05-30 04:39:23 +00:00
|
|
|
}
|
|
|
|
|
2011-05-30 23:41:56 +00:00
|
|
|
func wantsBlobInfo(req *http.Request) bool {
|
2013-09-15 19:12:26 +00:00
|
|
|
return httputil.IsGet(req) && blob.ValidRefString(req.FormValue("b"))
|
2011-05-30 23:41:56 +00:00
|
|
|
}
|
|
|
|
|
2013-12-31 23:25:19 +00:00
|
|
|
func getSuffixMatches(req *http.Request, pattern *regexp.Regexp) bool {
|
2013-09-15 19:12:26 +00:00
|
|
|
if httputil.IsGet(req) {
|
2013-06-12 09:17:30 +00:00
|
|
|
suffix := httputil.PathSuffix(req)
|
2013-12-31 23:25:19 +00:00
|
|
|
return pattern.MatchString(suffix)
|
2013-12-29 05:27:28 +00:00
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2011-04-16 22:44:22 +00:00
|
|
|
func (ui *UIHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
2013-06-12 09:17:30 +00:00
|
|
|
suffix := httputil.PathSuffix(req)
|
2011-05-30 02:05:21 +00:00
|
|
|
|
2011-04-16 23:18:31 +00:00
|
|
|
rw.Header().Set("Vary", "Accept")
|
|
|
|
switch {
|
|
|
|
case wantsDiscovery(req):
|
2012-11-07 17:57:43 +00:00
|
|
|
ui.root.serveDiscovery(rw, req)
|
2011-05-26 04:56:48 +00:00
|
|
|
case wantsUploadHelper(req):
|
|
|
|
ui.serveUploadHelper(rw, req)
|
2011-05-31 17:20:28 +00:00
|
|
|
case strings.HasPrefix(suffix, "download/"):
|
|
|
|
ui.serveDownload(rw, req)
|
2011-06-15 08:42:18 +00:00
|
|
|
case strings.HasPrefix(suffix, "thumbnail/"):
|
|
|
|
ui.serveThumbnail(rw, req)
|
2011-07-17 15:50:55 +00:00
|
|
|
case strings.HasPrefix(suffix, "tree/"):
|
|
|
|
ui.serveFileTree(rw, req)
|
2014-02-11 17:39:32 +00:00
|
|
|
case strings.HasPrefix(suffix, "qr/"):
|
|
|
|
ui.serveQR(rw, req)
|
2013-12-31 23:25:19 +00:00
|
|
|
case getSuffixMatches(req, closurePattern):
|
2013-06-12 09:05:01 +00:00
|
|
|
ui.serveClosure(rw, req)
|
2014-09-04 15:30:59 +00:00
|
|
|
case getSuffixMatches(req, lessPattern):
|
2014-09-04 05:21:29 +00:00
|
|
|
ui.serveFromDiskOrStatic(rw, req, lessPattern, ui.fileLessHandler, lessstatic.Files)
|
2013-12-31 23:25:19 +00:00
|
|
|
case getSuffixMatches(req, reactPattern):
|
|
|
|
ui.serveFromDiskOrStatic(rw, req, reactPattern, ui.fileReactHandler, reactstatic.Files)
|
server/camlistored/ui: improve map aspect search and markers
Notably:
-do not load any markers on an empty search query, because that would
mean loading absolutely all of the items with a location, which seems
like a bad idea.
-use different markers for different nodes. For now, foursquare
checkins, file images, and files have their own marker.
-vendor in https://github.com/lvoogdt/Leaflet.awesome-markers to achieve
the above, which relies on Font Awesome, which we already have in.
icons available for the markers: http://fontawesome.io/icons/
-when no location can be inferred from the search query, set the view to
encompass all markers that were drawn.
-when a location search is known, draw a rectangle representing the
results zone.
-use thumber for image in marker popup
-use title, if possible, instead of blobRef for link text in marker
popup
-switch to directly using OpenStreetMap tiles, instead of MapBox ones.
https://storage.googleapis.com/camlistore-screenshots/Screenshot_20170622-232359.png
Change-Id: Ibc84fa988aea8b8d3a2588ee8790adf6d9b5ad7a
2017-06-21 16:36:15 +00:00
|
|
|
case getSuffixMatches(req, leafletPattern):
|
|
|
|
ui.serveFromDiskOrStatic(rw, req, leafletPattern, ui.fileLeafletHandler, leafletstatic.Files)
|
2013-12-31 23:25:19 +00:00
|
|
|
case getSuffixMatches(req, glitchPattern):
|
|
|
|
ui.serveFromDiskOrStatic(rw, req, glitchPattern, ui.fileGlitchHandler, glitchstatic.Files)
|
2014-04-05 05:59:43 +00:00
|
|
|
case getSuffixMatches(req, fontawesomePattern):
|
|
|
|
ui.serveFromDiskOrStatic(rw, req, fontawesomePattern, ui.fileFontawesomeHandler, fontawesomestatic.Files)
|
2016-06-06 23:53:46 +00:00
|
|
|
case getSuffixMatches(req, openSansPattern):
|
|
|
|
ui.serveFromDiskOrStatic(rw, req, openSansPattern, ui.fileOpenSansHandler, opensansstatic.Files)
|
2011-04-16 23:18:31 +00:00
|
|
|
default:
|
2011-04-17 23:01:41 +00:00
|
|
|
file := ""
|
2011-05-30 02:05:21 +00:00
|
|
|
if m := staticFilePattern.FindStringSubmatch(suffix); m != nil {
|
2011-04-17 23:01:41 +00:00
|
|
|
file = m[1]
|
2011-05-30 02:05:21 +00:00
|
|
|
} else {
|
2011-05-30 23:41:56 +00:00
|
|
|
switch {
|
2014-08-17 02:13:36 +00:00
|
|
|
case wantsBlobRef(req):
|
2014-08-15 06:04:37 +00:00
|
|
|
file = "index.html"
|
2011-05-30 23:41:56 +00:00
|
|
|
case wantsPermanode(req):
|
|
|
|
file = "permanode.html"
|
|
|
|
case wantsBlobInfo(req):
|
|
|
|
file = "blobinfo.html"
|
2013-06-12 09:17:30 +00:00
|
|
|
case req.URL.Path == httputil.PathBase(req):
|
2012-12-12 02:29:58 +00:00
|
|
|
file = "index.html"
|
2011-05-30 23:41:56 +00:00
|
|
|
default:
|
2013-06-11 09:50:56 +00:00
|
|
|
http.Error(rw, "Illegal URL.", http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2013-07-04 23:07:50 +00:00
|
|
|
if file == "deps.js" {
|
2013-06-20 21:58:12 +00:00
|
|
|
serveDepsJS(rw, req, ui.uiDir)
|
|
|
|
return
|
2011-04-16 23:18:31 +00:00
|
|
|
}
|
2014-06-14 20:14:34 +00:00
|
|
|
ServeStaticFile(rw, req, uistatic.Files, file)
|
2011-04-16 22:44:22 +00:00
|
|
|
}
|
2011-04-16 23:18:31 +00:00
|
|
|
}
|
|
|
|
|
2014-06-14 20:14:34 +00:00
|
|
|
// ServeStaticFile serves file from the root virtual filesystem.
|
|
|
|
func ServeStaticFile(rw http.ResponseWriter, req *http.Request, root http.FileSystem, file string) {
|
2012-12-12 02:29:58 +00:00
|
|
|
f, err := root.Open("/" + file)
|
|
|
|
if err != nil {
|
|
|
|
http.NotFound(rw, req)
|
2013-12-29 05:27:28 +00:00
|
|
|
log.Printf("Failed to open file %q from embedded resources: %v", file, err)
|
2012-12-12 02:29:58 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
var modTime time.Time
|
|
|
|
if fi, err := f.Stat(); err == nil {
|
|
|
|
modTime = fi.ModTime()
|
|
|
|
}
|
2013-12-28 03:12:37 +00:00
|
|
|
// TODO(wathiede): should pkg/magic be leveraged here somehow? It has a
|
|
|
|
// slightly different purpose.
|
|
|
|
if strings.HasSuffix(file, ".svg") {
|
|
|
|
rw.Header().Set("Content-Type", "image/svg+xml")
|
|
|
|
}
|
2012-12-12 02:29:58 +00:00
|
|
|
http.ServeContent(rw, req, file, modTime, f)
|
|
|
|
}
|
|
|
|
|
2015-05-02 12:26:33 +00:00
|
|
|
func (ui *UIHandler) discovery() *camtypes.UIDiscovery {
|
|
|
|
pubRoots := map[string]*camtypes.PublishRootDiscovery{}
|
2014-06-14 20:14:34 +00:00
|
|
|
for _, v := range ui.publishRoots {
|
2015-05-02 12:26:33 +00:00
|
|
|
rd := &camtypes.PublishRootDiscovery{
|
|
|
|
Name: v.Name,
|
|
|
|
Prefix: []string{v.Prefix},
|
|
|
|
CurrentPermanode: v.Permanode,
|
2011-07-01 21:31:41 +00:00
|
|
|
}
|
2015-05-02 12:26:33 +00:00
|
|
|
pubRoots[v.Name] = rd
|
2011-06-19 21:12:56 +00:00
|
|
|
}
|
|
|
|
|
2017-08-30 17:32:14 +00:00
|
|
|
mapClustering, _ := strconv.ParseBool(os.Getenv("CAMLI_DEV_MAP_CLUSTERING"))
|
2015-05-02 12:26:33 +00:00
|
|
|
uiDisco := &camtypes.UIDiscovery{
|
|
|
|
UIRoot: ui.prefix,
|
|
|
|
UploadHelper: ui.prefix + "?camli.mode=uploadhelper",
|
|
|
|
DownloadHelper: path.Join(ui.prefix, "download") + "/",
|
|
|
|
DirectoryHelper: path.Join(ui.prefix, "tree") + "/",
|
|
|
|
PublishRoots: pubRoots,
|
2017-08-30 17:32:14 +00:00
|
|
|
MapClustering: mapClustering,
|
2012-11-07 17:57:43 +00:00
|
|
|
}
|
2015-05-02 12:26:33 +00:00
|
|
|
return uiDisco
|
2011-04-16 22:44:22 +00:00
|
|
|
}
|
2011-05-26 04:56:48 +00:00
|
|
|
|
pkg/server: add files "zipper" to DownloadHandler
We want to add a feature for clients (the web UI), where they can select
a bunch of files and ask the server for a zip archive of all these files.
This CL modifies the DownloadHandler so it does exactly that upon
reception of a POST request with a query parameter of the form
files=sha1-foo,sha1-bar,sha1-baz
This CL also adds a new button to the contextual sidebar of the web UI,
that takes care of sending the download request to the server.
known limitations: only permanodes with file as camliContent are
accepted as a valid selection (i.e. no sets, or static-dirs, etc) for
now.
Implementation detail:
We're creating an ephemeral DOM form on the fly to send the request.
The reason is: if we sent it as a Go http request, we'd have to read
the response manually and then we'd have no way of writing it to disk.
If we did it with an xhr, we could write the response to disk by
creating a File or Blob and then using URL.createObjectURL(), but we'd
have to keep the response in memory while doing so, which is
unacceptable for large enough archives.
Fixes #899
Change-Id: I104f7c5bd10ab3369e28d33752380dd12b5b3e6b
2017-03-06 20:02:19 +00:00
|
|
|
func (ui *UIHandler) serveDownload(w http.ResponseWriter, r *http.Request) {
|
2012-11-07 17:57:43 +00:00
|
|
|
if ui.root.Storage == nil {
|
pkg/server: add files "zipper" to DownloadHandler
We want to add a feature for clients (the web UI), where they can select
a bunch of files and ask the server for a zip archive of all these files.
This CL modifies the DownloadHandler so it does exactly that upon
reception of a POST request with a query parameter of the form
files=sha1-foo,sha1-bar,sha1-baz
This CL also adds a new button to the contextual sidebar of the web UI,
that takes care of sending the download request to the server.
known limitations: only permanodes with file as camliContent are
accepted as a valid selection (i.e. no sets, or static-dirs, etc) for
now.
Implementation detail:
We're creating an ephemeral DOM form on the fly to send the request.
The reason is: if we sent it as a Go http request, we'd have to read
the response manually and then we'd have no way of writing it to disk.
If we did it with an xhr, we could write the response to disk by
creating a File or Blob and then using URL.createObjectURL(), but we'd
have to keep the response in memory while doing so, which is
unacceptable for large enough archives.
Fixes #899
Change-Id: I104f7c5bd10ab3369e28d33752380dd12b5b3e6b
2017-03-06 20:02:19 +00:00
|
|
|
http.Error(w, "No BlobRoot configured", 500)
|
2011-05-31 17:20:28 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2011-07-03 17:53:02 +00:00
|
|
|
dh := &DownloadHandler{
|
2017-05-12 23:21:17 +00:00
|
|
|
// TODO(mpl): for more efficiency, the cache itself should be a
|
|
|
|
// blobpacked, or really anything better optimized for file reading
|
|
|
|
// than a blobserver.localdisk (which is what ui.Cache most likely is).
|
|
|
|
Fetcher: cacher.NewCachingFetcher(ui.Cache, ui.root.Storage),
|
2015-02-05 05:07:59 +00:00
|
|
|
Search: ui.search,
|
2011-06-06 16:07:07 +00:00
|
|
|
}
|
pkg/server: add files "zipper" to DownloadHandler
We want to add a feature for clients (the web UI), where they can select
a bunch of files and ask the server for a zip archive of all these files.
This CL modifies the DownloadHandler so it does exactly that upon
reception of a POST request with a query parameter of the form
files=sha1-foo,sha1-bar,sha1-baz
This CL also adds a new button to the contextual sidebar of the web UI,
that takes care of sending the download request to the server.
known limitations: only permanodes with file as camliContent are
accepted as a valid selection (i.e. no sets, or static-dirs, etc) for
now.
Implementation detail:
We're creating an ephemeral DOM form on the fly to send the request.
The reason is: if we sent it as a Go http request, we'd have to read
the response manually and then we'd have no way of writing it to disk.
If we did it with an xhr, we could write the response to disk by
creating a File or Blob and then using URL.createObjectURL(), but we'd
have to keep the response in memory while doing so, which is
unacceptable for large enough archives.
Fixes #899
Change-Id: I104f7c5bd10ab3369e28d33752380dd12b5b3e6b
2017-03-06 20:02:19 +00:00
|
|
|
dh.ServeHTTP(w, r)
|
2011-05-31 17:20:28 +00:00
|
|
|
}
|
2011-06-15 08:42:18 +00:00
|
|
|
|
|
|
|
func (ui *UIHandler) serveThumbnail(rw http.ResponseWriter, req *http.Request) {
|
2012-11-07 17:57:43 +00:00
|
|
|
if ui.root.Storage == nil {
|
2011-06-15 08:42:18 +00:00
|
|
|
http.Error(rw, "No BlobRoot configured", 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-06-12 09:17:30 +00:00
|
|
|
suffix := httputil.PathSuffix(req)
|
2011-06-15 08:42:18 +00:00
|
|
|
m := thumbnailPattern.FindStringSubmatch(suffix)
|
|
|
|
if m == nil {
|
|
|
|
httputil.ErrorRouting(rw, req)
|
|
|
|
return
|
|
|
|
}
|
2011-06-19 20:09:43 +00:00
|
|
|
|
2011-06-15 08:42:18 +00:00
|
|
|
query := req.URL.Query()
|
2013-07-29 08:02:40 +00:00
|
|
|
width, _ := strconv.Atoi(query.Get("mw"))
|
|
|
|
height, _ := strconv.Atoi(query.Get("mh"))
|
2013-08-04 02:54:30 +00:00
|
|
|
blobref, ok := blob.Parse(m[1])
|
|
|
|
if !ok {
|
2011-06-15 08:42:18 +00:00
|
|
|
http.Error(rw, "Invalid blobref", 400)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-07-29 08:02:40 +00:00
|
|
|
if width == 0 {
|
|
|
|
width = search.MaxImageSize
|
|
|
|
}
|
|
|
|
if height == 0 {
|
|
|
|
height = search.MaxImageSize
|
|
|
|
}
|
|
|
|
|
2011-07-03 20:33:41 +00:00
|
|
|
th := &ImageHandler{
|
2012-11-07 17:57:43 +00:00
|
|
|
Fetcher: ui.root.Storage,
|
2011-07-03 20:33:41 +00:00
|
|
|
Cache: ui.Cache,
|
|
|
|
MaxWidth: width,
|
|
|
|
MaxHeight: height,
|
2014-06-14 20:14:34 +00:00
|
|
|
ThumbMeta: ui.thumbMeta,
|
|
|
|
ResizeSem: ui.resizeSem,
|
2015-04-13 17:45:54 +00:00
|
|
|
Search: ui.search,
|
2011-06-15 08:42:18 +00:00
|
|
|
}
|
2011-07-03 20:33:41 +00:00
|
|
|
th.ServeHTTP(rw, req, blobref)
|
2011-06-15 08:42:18 +00:00
|
|
|
}
|
2011-07-17 15:50:55 +00:00
|
|
|
|
|
|
|
func (ui *UIHandler) serveFileTree(rw http.ResponseWriter, req *http.Request) {
|
2012-11-07 17:57:43 +00:00
|
|
|
if ui.root.Storage == nil {
|
2011-07-17 15:50:55 +00:00
|
|
|
http.Error(rw, "No BlobRoot configured", 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-06-12 09:17:30 +00:00
|
|
|
suffix := httputil.PathSuffix(req)
|
2011-07-17 15:50:55 +00:00
|
|
|
m := treePattern.FindStringSubmatch(suffix)
|
|
|
|
if m == nil {
|
|
|
|
httputil.ErrorRouting(rw, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2013-08-04 02:54:30 +00:00
|
|
|
blobref, ok := blob.Parse(m[1])
|
|
|
|
if !ok {
|
2011-07-17 15:50:55 +00:00
|
|
|
http.Error(rw, "Invalid blobref", 400)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
fth := &FileTreeHandler{
|
2012-11-07 17:57:43 +00:00
|
|
|
Fetcher: ui.root.Storage,
|
2011-08-25 15:14:47 +00:00
|
|
|
file: blobref,
|
2011-07-17 15:50:55 +00:00
|
|
|
}
|
|
|
|
fth.ServeHTTP(rw, req)
|
|
|
|
}
|
2012-12-13 18:20:45 +00:00
|
|
|
|
2013-06-12 09:05:01 +00:00
|
|
|
func (ui *UIHandler) serveClosure(rw http.ResponseWriter, req *http.Request) {
|
2013-06-12 09:17:30 +00:00
|
|
|
suffix := httputil.PathSuffix(req)
|
2012-12-23 19:01:19 +00:00
|
|
|
if ui.closureHandler == nil {
|
2013-06-11 09:50:56 +00:00
|
|
|
log.Printf("%v not served: closure handler is nil", suffix)
|
2012-12-13 18:20:45 +00:00
|
|
|
http.NotFound(rw, req)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
m := closurePattern.FindStringSubmatch(suffix)
|
2013-06-11 09:50:56 +00:00
|
|
|
if m == nil {
|
|
|
|
httputil.ErrorRouting(rw, req)
|
2012-12-23 20:39:15 +00:00
|
|
|
return
|
|
|
|
}
|
2013-06-11 09:50:56 +00:00
|
|
|
req.URL.Path = "/" + m[1]
|
|
|
|
ui.closureHandler.ServeHTTP(rw, req)
|
2012-12-13 18:20:45 +00:00
|
|
|
}
|
2012-12-23 20:39:15 +00:00
|
|
|
|
2013-12-31 23:25:19 +00:00
|
|
|
// serveFromDiskOrStatic matches rx against req's path and serves the match either from disk (if non-nil) or from static (embedded in the binary).
|
|
|
|
func (ui *UIHandler) serveFromDiskOrStatic(rw http.ResponseWriter, req *http.Request, rx *regexp.Regexp, disk http.Handler, static *fileembed.Files) {
|
2013-12-29 05:27:28 +00:00
|
|
|
suffix := httputil.PathSuffix(req)
|
2013-12-31 23:25:19 +00:00
|
|
|
m := rx.FindStringSubmatch(suffix)
|
2013-12-29 05:27:28 +00:00
|
|
|
if m == nil {
|
2013-12-31 23:25:19 +00:00
|
|
|
panic("Caller should verify that rx matches")
|
2013-12-29 05:27:28 +00:00
|
|
|
}
|
|
|
|
file := m[1]
|
2013-12-31 23:25:19 +00:00
|
|
|
if disk != nil {
|
2013-12-29 05:27:28 +00:00
|
|
|
req.URL.Path = "/" + file
|
2013-12-31 23:25:19 +00:00
|
|
|
disk.ServeHTTP(rw, req)
|
2013-12-29 05:27:28 +00:00
|
|
|
} else {
|
2014-06-14 20:14:34 +00:00
|
|
|
ServeStaticFile(rw, req, static, file)
|
2013-12-29 05:27:28 +00:00
|
|
|
}
|
2013-12-31 23:25:19 +00:00
|
|
|
|
2013-12-29 05:27:28 +00:00
|
|
|
}
|
|
|
|
|
2014-02-11 17:39:32 +00:00
|
|
|
func (ui *UIHandler) serveQR(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
url := req.URL.Query().Get("url")
|
|
|
|
if url == "" {
|
|
|
|
http.Error(rw, "Missing url parameter.", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
code, err := qr.Encode(url, qr.L)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
rw.Header().Set("Content-Type", "image/png")
|
|
|
|
rw.Write(code.PNG())
|
|
|
|
}
|
|
|
|
|
2012-12-23 20:39:15 +00:00
|
|
|
// serveDepsJS serves an auto-generated Closure deps.js file.
|
2013-06-20 21:58:12 +00:00
|
|
|
func serveDepsJS(rw http.ResponseWriter, req *http.Request, dir string) {
|
2013-07-04 23:07:50 +00:00
|
|
|
var root http.FileSystem
|
|
|
|
if dir == "" {
|
|
|
|
root = uistatic.Files
|
|
|
|
} else {
|
|
|
|
root = http.Dir(dir)
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := closure.GenDeps(root)
|
2012-12-23 20:39:15 +00:00
|
|
|
if err != nil {
|
2013-06-13 00:21:29 +00:00
|
|
|
log.Print(err)
|
2012-12-23 20:39:15 +00:00
|
|
|
http.Error(rw, "Server error", 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
rw.Header().Set("Content-Type", "text/javascript; charset=utf-8")
|
2013-06-20 21:58:12 +00:00
|
|
|
rw.Write([]byte("// auto-generated from camlistored\n"))
|
2013-06-13 00:21:29 +00:00
|
|
|
rw.Write(b)
|
2012-12-23 20:39:15 +00:00
|
|
|
}
|