mirror of https://github.com/perkeep/perkeep.git
331 lines
10 KiB
Go
331 lines
10 KiB
Go
/*
|
|
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.
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"http"
|
|
"json"
|
|
"log"
|
|
"strings"
|
|
"os"
|
|
|
|
"camli/auth"
|
|
"camli/blobref"
|
|
"camli/blobserver"
|
|
"camli/blobserver/handlers"
|
|
"camli/errorutil"
|
|
"camli/httputil"
|
|
"camli/jsonconfig"
|
|
"camli/osutil"
|
|
"camli/search"
|
|
"camli/webserver"
|
|
|
|
// Storage options:
|
|
"camli/blobserver/localdisk"
|
|
_ "camli/blobserver/s3"
|
|
_ "camli/mysqlindexer" // indexer, but uses storage interface
|
|
)
|
|
|
|
var flagUseConfigFiles = flag.Bool("useconfigfiles", false,
|
|
"Use the ~/.camli/config files and enable the /config HTTP handler." +
|
|
"+If false, all configuration is done ")
|
|
var flagPasswordFile = flag.String("passwordfile", "password.txt",
|
|
"Password file, relative to the ~USER/.camli/ directory.")
|
|
|
|
// If useConfigFiles is off:
|
|
var flagStorageRoot = flag.String("root", "/tmp/camliroot", "Root directory to store files")
|
|
var flagQueuePartitions = flag.String("queue-partitions", "queue-indexer",
|
|
"Comma-separated list of queue partitions to reference uploaded blobs into. "+
|
|
"Typically one for your indexer and one per mirror full syncer.")
|
|
|
|
// TODO: Temporary
|
|
var flagRequestLog = flag.Bool("reqlog", false, "Log incoming requests")
|
|
|
|
const camliPrefix = "/camli/"
|
|
|
|
var ErrCamliPath = os.NewError("Invalid Camlistore request path")
|
|
|
|
func parseCamliPath(path string) (action string, err os.Error) {
|
|
camIdx := strings.Index(path, camliPrefix)
|
|
if camIdx == -1 {
|
|
return "", ErrCamliPath
|
|
}
|
|
action = path[camIdx+len(camliPrefix):]
|
|
return
|
|
}
|
|
|
|
func unsupportedHandler(conn http.ResponseWriter, req *http.Request) {
|
|
httputil.BadRequestError(conn, "Unsupported camlistore path or method.")
|
|
}
|
|
|
|
// where prefix is like "/" or "/s3/" for e.g. "/camli/" or "/s3/camli/*"
|
|
func makeCamliHandler(prefix, baseURL string, storage blobserver.Storage) http.Handler {
|
|
return http.HandlerFunc(func(conn http.ResponseWriter, req *http.Request) {
|
|
action, err := parseCamliPath(req.URL.Path[len(prefix)-1:])
|
|
if err != nil {
|
|
log.Printf("Invalid request for method %q, path %q",
|
|
req.Method, req.URL.Path)
|
|
unsupportedHandler(conn, req)
|
|
return
|
|
}
|
|
// TODO: actually deal with partitions here
|
|
part := &partitionConfig{"", true, true, false, nil, baseURL + prefix[:len(prefix)-1]}
|
|
handleCamliUsingStorage(conn, req, action, part, storage)
|
|
})
|
|
}
|
|
|
|
func handleCamliUsingStorage(conn http.ResponseWriter, req *http.Request, action string, partition blobserver.Partition, storage blobserver.Storage) {
|
|
handler := unsupportedHandler
|
|
if *flagRequestLog {
|
|
log.Printf("method %q; partition %q; action %q", req.Method, partition, action)
|
|
}
|
|
switch req.Method {
|
|
case "GET":
|
|
switch action {
|
|
case "enumerate-blobs":
|
|
handler = auth.RequireAuth(handlers.CreateEnumerateHandler(storage, partition))
|
|
case "stat":
|
|
handler = auth.RequireAuth(handlers.CreateStatHandler(storage, partition))
|
|
default:
|
|
handler = handlers.CreateGetHandler(storage)
|
|
}
|
|
case "POST":
|
|
switch action {
|
|
case "stat":
|
|
handler = auth.RequireAuth(handlers.CreateStatHandler(storage, partition))
|
|
case "upload":
|
|
handler = auth.RequireAuth(handlers.CreateUploadHandler(storage, partition))
|
|
case "remove":
|
|
// Currently only allows removing from a non-main partition.
|
|
handler = auth.RequireAuth(handlers.CreateRemoveHandler(storage, partition))
|
|
}
|
|
case "PUT": // no longer part of spec
|
|
handler = auth.RequireAuth(handlers.CreateNonStandardPutHandler(storage, partition))
|
|
}
|
|
handler(conn, req)
|
|
}
|
|
|
|
func exitFailure(pattern string, args ...interface{}) {
|
|
if !strings.HasSuffix(pattern, "\n") {
|
|
pattern = pattern + "\n"
|
|
}
|
|
fmt.Fprintf(os.Stderr, pattern, args...)
|
|
os.Exit(1)
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
if *flagUseConfigFiles {
|
|
configFileMain()
|
|
return
|
|
}
|
|
|
|
commandLineConfigurationMain()
|
|
}
|
|
|
|
func commandLineConfigurationMain() {
|
|
auth.AccessPassword = os.Getenv("CAMLI_PASSWORD")
|
|
if len(auth.AccessPassword) == 0 {
|
|
exitFailure("No CAMLI_PASSWORD environment variable set.")
|
|
}
|
|
|
|
if *flagStorageRoot == "" {
|
|
exitFailure("No storage root specified in --root")
|
|
}
|
|
|
|
storage, err := localdisk.New(*flagStorageRoot)
|
|
if err != nil {
|
|
exitFailure("Error for --root of %q: %v", *flagStorageRoot, err)
|
|
}
|
|
|
|
for _, partName := range strings.Split(*flagQueuePartitions, ",", -1) {
|
|
log.Printf("TODO: partition %q requested by command-line mode is broken at the moment", partName)
|
|
// TODO: get this working again
|
|
//part := &partitionConfig{name: partName, writable: false, readable: true, queue: true}
|
|
// part.urlbase = "/partition-" + partName
|
|
}
|
|
|
|
ws := webserver.New()
|
|
|
|
const partitionPrefix = "/partition-"
|
|
pickPartitionHandlerMaybe := func(req *http.Request) (handler http.HandlerFunc, intercept bool) {
|
|
if !strings.HasPrefix(req.URL.Path, partitionPrefix) {
|
|
intercept = false
|
|
return
|
|
}
|
|
panic("TODO: re-implement, maybe")
|
|
//return http.HandlerFunc(handleCamli), true
|
|
}
|
|
|
|
ws.RegisterPreMux(webserver.HandlerPicker(pickPartitionHandlerMaybe))
|
|
ws.Handle("/camli/", makeCamliHandler("/", ws.BaseURL(), storage))
|
|
|
|
//// TODO: temporary
|
|
if false {
|
|
//ownerBlobRef := client.SignerPublicKeyBlobref()
|
|
//if ownerBlobRef == nil {
|
|
// log.Fatalf("Public key not configured.")
|
|
//}
|
|
|
|
//ws.HandleFunc("/camli/search", func(conn http.ResponseWriter, req *http.Request) {
|
|
// handler := auth.RequireAuth(search.CreateHandler(myIndexer, ownerBlobRef))
|
|
// handler(conn, req)
|
|
//})
|
|
}
|
|
|
|
root := &RootHandler{OfferSetup: true}
|
|
ws.Handle("/", root)
|
|
ws.HandleFunc("/setup", setupHome)
|
|
ws.Serve()
|
|
}
|
|
|
|
func configFileMain() {
|
|
f, err := os.Open(osutil.UserServerConfigPath())
|
|
if err != nil {
|
|
exitFailure("error opening %s: %v", osutil.UserServerConfigPath(), err)
|
|
}
|
|
defer f.Close()
|
|
dj := json.NewDecoder(f)
|
|
config := make(map[string]interface{})
|
|
if err = dj.Decode(&config); err != nil {
|
|
extra := ""
|
|
if serr, ok := err.(*json.SyntaxError); ok {
|
|
if _, serr := f.Seek(0, os.SEEK_SET); serr != nil {
|
|
log.Fatalf("seek error: %v", serr)
|
|
}
|
|
line, col, highlight := errorutil.HighlightBytePosition(f, serr.Offset)
|
|
extra = fmt.Sprintf(":\nError at line %d, column %d (file offset %d):\n%s",
|
|
line, col, serr.Offset, highlight)
|
|
}
|
|
exitFailure("error parsing JSON object in config file %s%s\n%v",
|
|
osutil.UserServerConfigPath(), extra, err)
|
|
}
|
|
|
|
ws := webserver.New()
|
|
baseURL := ws.BaseURL()
|
|
|
|
if password, ok := config["password"].(string); ok {
|
|
auth.AccessPassword = password
|
|
}
|
|
|
|
if url, ok := config["baseURL"].(string); ok {
|
|
baseURL = url
|
|
}
|
|
|
|
prefixes, ok := config["prefixes"].(map[string]interface{})
|
|
if !ok {
|
|
exitFailure("No top-level \"prefixes\": {...} in %s", osutil.UserServerConfigPath)
|
|
}
|
|
|
|
createdHandlers := make(map[string]interface{})
|
|
|
|
for prefix, vei := range prefixes {
|
|
if !strings.HasPrefix(prefix, "/") {
|
|
exitFailure("prefix %q doesn't start with /", prefix)
|
|
}
|
|
if !strings.HasSuffix(prefix, "/") {
|
|
exitFailure("prefix %q doesn't end with /", prefix)
|
|
}
|
|
pconf, ok := vei.(map[string]interface{})
|
|
if !ok {
|
|
exitFailure("prefix %q value isn't an object", prefix)
|
|
}
|
|
handlerType, ok := pconf["handler"].(string)
|
|
if !ok {
|
|
exitFailure("in prefix %q, expected the \"handler\" of prefix %q to be a string",
|
|
prefix)
|
|
}
|
|
handlerArgs, ok := pconf["handlerArgs"].(map[string]interface{})
|
|
if !ok {
|
|
if _, present := pconf["handlerArgs"]; !present {
|
|
handlerArgs = make(jsonconfig.Obj)
|
|
} else {
|
|
exitFailure("in prefix %q, expected the \"handlerArgs\" to be a JSON object",
|
|
prefix)
|
|
}
|
|
}
|
|
installHandler := func(creator func (conf jsonconfig.Obj) (h http.Handler, err os.Error)) {
|
|
h, err := creator(jsonconfig.Obj(handlerArgs))
|
|
if err != nil {
|
|
exitFailure("error instantiating handler for prefix %s: %v",
|
|
prefix, err)
|
|
}
|
|
createdHandlers[prefix] = h
|
|
ws.Handle(prefix, h)
|
|
}
|
|
switch {
|
|
case handlerType == "search":
|
|
// Skip it this round. Get it in second pass
|
|
// to ensure the search's dependent indexer
|
|
// has been created.
|
|
case handlerType == "root":
|
|
installHandler(createRootHandler)
|
|
case handlerType == "ui":
|
|
installHandler(createUIHandler)
|
|
default:
|
|
// Assume a storage interface
|
|
pstorage, err := blobserver.CreateStorage(handlerType, jsonconfig.Obj(handlerArgs))
|
|
if err != nil {
|
|
exitFailure("error instantiating storage for prefix %q, type %q: %v",
|
|
prefix, handlerType, err)
|
|
}
|
|
createdHandlers[prefix] = pstorage
|
|
ws.Handle(prefix + "camli/", makeCamliHandler(prefix, baseURL, pstorage))
|
|
}
|
|
}
|
|
|
|
// Another pass for search handler(s)
|
|
for prefix, vei := range prefixes {
|
|
if _, alreadyCreated := createdHandlers[prefix]; alreadyCreated {
|
|
continue
|
|
}
|
|
pconf := vei.(map[string]interface{})
|
|
handlerType := pconf["handler"].(string)
|
|
config := jsonconfig.Obj(pconf["handlerArgs"].(map[string]interface{}))
|
|
checkConfig := func() {
|
|
if err := config.Validate(); err != nil {
|
|
exitFailure("configuration error in \"handlerArgs\" for prefix %s: %v", err)
|
|
}
|
|
}
|
|
switch {
|
|
case handlerType == "search":
|
|
indexPrefix := config.RequiredString("index") // TODO: add optional help tips here?
|
|
ownerBlobStr := config.RequiredString("owner")
|
|
checkConfig()
|
|
indexer, ok := createdHandlers[indexPrefix].(search.Index)
|
|
if !ok {
|
|
exitFailure("prefix %q references invalid indexer %q", prefix, indexPrefix)
|
|
}
|
|
ownerBlobRef := blobref.Parse(ownerBlobStr)
|
|
if ownerBlobRef == nil {
|
|
exitFailure("prefix %q references has malformed blobref %q; expecting e.g. sha1-xxxxxxxxxxxx",
|
|
prefix, ownerBlobStr)
|
|
}
|
|
h := auth.RequireAuth(search.CreateHandler(indexer, ownerBlobRef))
|
|
ws.HandleFunc(prefix + "camli/", h)
|
|
default:
|
|
panic("unexpected handlerType: " + handlerType)
|
|
}
|
|
|
|
}
|
|
|
|
ws.Serve()
|
|
}
|