github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/pkg/serverconfig/genconfig.go (about)

     1  /*
     2  Copyright 2012 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 serverconfig
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"net/url"
    23  	"os"
    24  	"path/filepath"
    25  	"strings"
    26  
    27  	"camlistore.org/pkg/blob"
    28  	"camlistore.org/pkg/jsonconfig"
    29  	"camlistore.org/pkg/jsonsign"
    30  	"camlistore.org/pkg/osutil"
    31  )
    32  
    33  // various parameters derived from the high-level user config
    34  // and needed to set up the low-level config.
    35  type configPrefixesParams struct {
    36  	secretRing       string
    37  	keyId            string
    38  	indexerPath      string
    39  	blobPath         string
    40  	packBlobs        bool
    41  	searchOwner      blob.Ref
    42  	shareHandlerPath string
    43  	flickr           string
    44  	memoryIndex      bool
    45  }
    46  
    47  var (
    48  	tempDir = os.TempDir
    49  	noMkdir bool // for tests to not call os.Mkdir
    50  )
    51  
    52  func addPublishedConfig(prefixes jsonconfig.Obj,
    53  	published jsonconfig.Obj,
    54  	sourceRoot string) ([]interface{}, error) {
    55  	pubPrefixes := []interface{}{}
    56  	for k, v := range published {
    57  		p, ok := v.(map[string]interface{})
    58  		if !ok {
    59  			return nil, fmt.Errorf("Wrong type for %s; was expecting map[string]interface{}, got %T", k, v)
    60  		}
    61  		rootName := strings.Replace(k, "/", "", -1) + "Root"
    62  		rootPermanode, goTemplate, style, js := "", "", "", ""
    63  		for pk, pv := range p {
    64  			val, ok := pv.(string)
    65  			if !ok {
    66  				return nil, fmt.Errorf("Was expecting type string for %s, got %T", pk, pv)
    67  			}
    68  			switch pk {
    69  			case "rootPermanode":
    70  				rootPermanode = val
    71  			case "goTemplate":
    72  				goTemplate = val
    73  			case "style":
    74  				style = val
    75  			case "js":
    76  				js = val
    77  			default:
    78  				return nil, fmt.Errorf("Unexpected key %q in config for %s", pk, k)
    79  			}
    80  		}
    81  		if rootPermanode == "" || goTemplate == "" {
    82  			return nil, fmt.Errorf("Missing key in configuration for %s, need \"rootPermanode\" and \"goTemplate\"", k)
    83  		}
    84  		ob := map[string]interface{}{}
    85  		ob["handler"] = "publish"
    86  		handlerArgs := map[string]interface{}{
    87  			"rootName":      rootName,
    88  			"blobRoot":      "/bs-and-maybe-also-index/",
    89  			"searchRoot":    "/my-search/",
    90  			"cache":         "/cache/",
    91  			"rootPermanode": []interface{}{"/sighelper/", rootPermanode},
    92  		}
    93  		if sourceRoot != "" {
    94  			handlerArgs["sourceRoot"] = sourceRoot
    95  		}
    96  		handlerArgs["goTemplate"] = goTemplate
    97  		if style != "" {
    98  			handlerArgs["css"] = []interface{}{style}
    99  		}
   100  		if js != "" {
   101  			handlerArgs["js"] = []interface{}{js}
   102  		}
   103  		// TODO(mpl): we'll probably want to use osutil.CacheDir() if thumbnails.kv
   104  		// contains private info? same for some of the other "camli-cache" ones?
   105  		thumbsCacheDir := filepath.Join(tempDir(), "camli-cache")
   106  		handlerArgs["scaledImage"] = map[string]interface{}{
   107  			"type": "kv",
   108  			"file": filepath.Join(thumbsCacheDir, "thumbnails.kv"),
   109  		}
   110  		if err := os.MkdirAll(thumbsCacheDir, 0700); err != nil {
   111  			return nil, fmt.Errorf("Could not create cache dir %s: %v", thumbsCacheDir, err)
   112  		}
   113  		ob["handlerArgs"] = handlerArgs
   114  		prefixes[k] = ob
   115  		pubPrefixes = append(pubPrefixes, k)
   116  	}
   117  	return pubPrefixes, nil
   118  }
   119  
   120  func addUIConfig(params *configPrefixesParams,
   121  	prefixes jsonconfig.Obj,
   122  	uiPrefix string,
   123  	published []interface{},
   124  	sourceRoot string) {
   125  
   126  	args := map[string]interface{}{
   127  		"jsonSignRoot": "/sighelper/",
   128  		"cache":        "/cache/",
   129  	}
   130  	if len(published) > 0 {
   131  		args["publishRoots"] = published
   132  	}
   133  	if sourceRoot != "" {
   134  		args["sourceRoot"] = sourceRoot
   135  	}
   136  	if params.blobPath != "" {
   137  		args["scaledImage"] = map[string]interface{}{
   138  			"type": "kv",
   139  			"file": filepath.Join(params.blobPath, "thumbmeta.kv"),
   140  		}
   141  	}
   142  	prefixes[uiPrefix] = map[string]interface{}{
   143  		"handler":     "ui",
   144  		"handlerArgs": args,
   145  	}
   146  }
   147  
   148  func addMongoConfig(prefixes jsonconfig.Obj, dbname string, dbinfo string) {
   149  	fields := strings.Split(dbinfo, "@")
   150  	if len(fields) != 2 {
   151  		exitFailure("Malformed mongo config string. Got \"%v\", want: \"user:password@host\"", dbinfo)
   152  	}
   153  	host := fields[1]
   154  	fields = strings.Split(fields[0], ":")
   155  	if len(fields) != 2 {
   156  		exitFailure("Malformed mongo config string. Got \"%v\", want: \"user:password\"", fields[0])
   157  	}
   158  	ob := map[string]interface{}{}
   159  	ob["enabled"] = true
   160  	ob["handler"] = "storage-mongodbindexer"
   161  	ob["handlerArgs"] = map[string]interface{}{
   162  		"host":       host,
   163  		"user":       fields[0],
   164  		"password":   fields[1],
   165  		"database":   dbname,
   166  		"blobSource": "/bs/",
   167  	}
   168  	prefixes["/index-mongo/"] = ob
   169  }
   170  
   171  func addSQLConfig(rdbms string, prefixes jsonconfig.Obj, dbname string, dbinfo string) {
   172  	fields := strings.Split(dbinfo, "@")
   173  	if len(fields) != 2 {
   174  		exitFailure("Malformed " + rdbms + " config string. Want: \"user@host:password\"")
   175  	}
   176  	user := fields[0]
   177  	fields = strings.Split(fields[1], ":")
   178  	if len(fields) != 2 {
   179  		exitFailure("Malformed " + rdbms + " config string. Want: \"user@host:password\"")
   180  	}
   181  	ob := map[string]interface{}{}
   182  	ob["enabled"] = true
   183  	ob["handler"] = "storage-" + rdbms + "indexer"
   184  	ob["handlerArgs"] = map[string]interface{}{
   185  		"host":       fields[0],
   186  		"user":       user,
   187  		"password":   fields[1],
   188  		"database":   dbname,
   189  		"blobSource": "/bs/",
   190  	}
   191  	prefixes["/index-"+rdbms+"/"] = ob
   192  }
   193  
   194  func addPostgresConfig(prefixes jsonconfig.Obj, dbname string, dbinfo string) {
   195  	addSQLConfig("postgres", prefixes, dbname, dbinfo)
   196  }
   197  
   198  func addMySQLConfig(prefixes jsonconfig.Obj, dbname string, dbinfo string) {
   199  	addSQLConfig("mysql", prefixes, dbname, dbinfo)
   200  }
   201  
   202  func addSQLiteConfig(prefixes jsonconfig.Obj, file string) {
   203  	ob := map[string]interface{}{}
   204  	ob["handler"] = "storage-sqliteindexer"
   205  	ob["handlerArgs"] = map[string]interface{}{
   206  		"blobSource": "/bs/",
   207  		"file":       file,
   208  	}
   209  	prefixes["/index-sqlite/"] = ob
   210  }
   211  
   212  func addKVConfig(prefixes jsonconfig.Obj, file string) {
   213  	prefixes["/index-kv/"] = map[string]interface{}{
   214  		"handler": "storage-kvfileindexer",
   215  		"handlerArgs": map[string]interface{}{
   216  			"blobSource": "/bs/",
   217  			"file":       file,
   218  		},
   219  	}
   220  }
   221  
   222  func addS3Config(params *configPrefixesParams, prefixes jsonconfig.Obj, s3 string) error {
   223  	f := strings.SplitN(s3, ":", 4)
   224  	if len(f) < 3 {
   225  		return errors.New(`genconfig: expected "s3" field to be of form "access_key_id:secret_access_key:bucket"`)
   226  	}
   227  	accessKey, secret, bucket := f[0], f[1], f[2]
   228  	var hostname string
   229  	if len(f) == 4 {
   230  		hostname = f[3]
   231  	}
   232  	isPrimary := false
   233  	if _, ok := prefixes["/bs/"]; !ok {
   234  		isPrimary = true
   235  	}
   236  	s3Prefix := ""
   237  	if isPrimary {
   238  		s3Prefix = "/bs/"
   239  	} else {
   240  		s3Prefix = "/sto-s3/"
   241  	}
   242  	args := map[string]interface{}{
   243  		"aws_access_key":        accessKey,
   244  		"aws_secret_access_key": secret,
   245  		"bucket":                bucket,
   246  	}
   247  	if hostname != "" {
   248  		args["hostname"] = hostname
   249  	}
   250  	prefixes[s3Prefix] = map[string]interface{}{
   251  		"handler":     "storage-s3",
   252  		"handlerArgs": args,
   253  	}
   254  	if isPrimary {
   255  		// TODO(mpl): s3CacheBucket
   256  		// See http://code.google.com/p/camlistore/issues/detail?id=85
   257  		prefixes["/cache/"] = map[string]interface{}{
   258  			"handler": "storage-filesystem",
   259  			"handlerArgs": map[string]interface{}{
   260  				"path": filepath.Join(tempDir(), "camli-cache"),
   261  			},
   262  		}
   263  	} else {
   264  		if params.blobPath == "" {
   265  			panic("unexpected empty blobpath with sync-to-s3")
   266  		}
   267  		prefixes["/sync-to-s3/"] = map[string]interface{}{
   268  			"handler": "sync",
   269  			"handlerArgs": map[string]interface{}{
   270  				"from": "/bs/",
   271  				"to":   s3Prefix,
   272  				"queue": map[string]interface{}{
   273  					"type": "kv",
   274  					"file": filepath.Join(params.blobPath, "sync-to-s3-queue.kv"),
   275  				},
   276  			},
   277  		}
   278  	}
   279  	return nil
   280  }
   281  
   282  func addGoogleDriveConfig(prefixes jsonconfig.Obj, highCfg string) error {
   283  	f := strings.SplitN(highCfg, ":", 4)
   284  	if len(f) != 4 {
   285  		return errors.New(`genconfig: expected "googledrive" field to be of form "client_id:client_secret:refresh_token:parent_id"`)
   286  	}
   287  	clientId, secret, refreshToken, parentId := f[0], f[1], f[2], f[3]
   288  
   289  	isPrimary := false
   290  	if _, ok := prefixes["/bs/"]; !ok {
   291  		isPrimary = true
   292  	}
   293  
   294  	prefix := ""
   295  	if isPrimary {
   296  		prefix = "/bs/"
   297  	} else {
   298  		prefix = "/sto-googledrive/"
   299  	}
   300  	prefixes[prefix] = map[string]interface{}{
   301  		"handler": "storage-googledrive",
   302  		"handlerArgs": map[string]interface{}{
   303  			"parent_id": parentId,
   304  			"auth": map[string]interface{}{
   305  				"client_id":     clientId,
   306  				"client_secret": secret,
   307  				"refresh_token": refreshToken,
   308  			},
   309  		},
   310  	}
   311  
   312  	if isPrimary {
   313  		prefixes["/cache/"] = map[string]interface{}{
   314  			"handler": "storage-filesystem",
   315  			"handlerArgs": map[string]interface{}{
   316  				"path": filepath.Join(tempDir(), "camli-cache"),
   317  			},
   318  		}
   319  	} else {
   320  		prefixes["/sync-to-googledrive/"] = map[string]interface{}{
   321  			"handler": "sync",
   322  			"handlerArgs": map[string]interface{}{
   323  				"from": "/bs/",
   324  				"to":   prefix,
   325  			},
   326  		}
   327  	}
   328  
   329  	return nil
   330  }
   331  
   332  func addGoogleCloudStorageConfig(prefixes jsonconfig.Obj, highCfg string) error {
   333  	f := strings.SplitN(highCfg, ":", 4)
   334  	if len(f) != 4 {
   335  		return errors.New(`genconfig: expected "googlecloudstorage" field to be of form "client_id:client_secret:refresh_token:bucket"`)
   336  	}
   337  	clientId, secret, refreshToken, bucket := f[0], f[1], f[2], f[3]
   338  
   339  	isPrimary := false
   340  	if _, ok := prefixes["/bs/"]; !ok {
   341  		isPrimary = true
   342  	}
   343  
   344  	gsPrefix := ""
   345  	if isPrimary {
   346  		gsPrefix = "/bs/"
   347  	} else {
   348  		gsPrefix = "/sto-googlecloudstorage/"
   349  	}
   350  
   351  	prefixes[gsPrefix] = map[string]interface{}{
   352  		"handler": "storage-googlecloudstorage",
   353  		"handlerArgs": map[string]interface{}{
   354  			"bucket": bucket,
   355  			"auth": map[string]interface{}{
   356  				"client_id":     clientId,
   357  				"client_secret": secret,
   358  				"refresh_token": refreshToken,
   359  				// If high-level config is for the common user then fullSyncOnStart = true
   360  				// Then the default just works.
   361  				//"fullSyncOnStart": true,
   362  				//"blockingFullSyncOnStart": false
   363  			},
   364  		},
   365  	}
   366  
   367  	if isPrimary {
   368  		// TODO: cacheBucket like s3CacheBucket?
   369  		prefixes["/cache/"] = map[string]interface{}{
   370  			"handler": "storage-filesystem",
   371  			"handlerArgs": map[string]interface{}{
   372  				"path": filepath.Join(tempDir(), "camli-cache"),
   373  			},
   374  		}
   375  	} else {
   376  		prefixes["/sync-to-googlecloudstorage/"] = map[string]interface{}{
   377  			"handler": "sync",
   378  			"handlerArgs": map[string]interface{}{
   379  				"from": "/bs/",
   380  				"to":   gsPrefix,
   381  			},
   382  		}
   383  	}
   384  	return nil
   385  }
   386  
   387  func genLowLevelPrefixes(params *configPrefixesParams, ownerName string) (m jsonconfig.Obj) {
   388  	m = make(jsonconfig.Obj)
   389  
   390  	haveIndex := params.indexerPath != ""
   391  	root := "/bs/"
   392  	pubKeyDest := root
   393  	if haveIndex {
   394  		root = "/bs-and-maybe-also-index/"
   395  		pubKeyDest = "/bs-and-index/"
   396  	}
   397  
   398  	rootArgs := map[string]interface{}{
   399  		"stealth":    false,
   400  		"blobRoot":   root,
   401  		"statusRoot": "/status/",
   402  	}
   403  	if ownerName != "" {
   404  		rootArgs["ownerName"] = ownerName
   405  	}
   406  	m["/"] = map[string]interface{}{
   407  		"handler":     "root",
   408  		"handlerArgs": rootArgs,
   409  	}
   410  	if haveIndex {
   411  		setMap(m, "/", "handlerArgs", "searchRoot", "/my-search/")
   412  	}
   413  
   414  	m["/setup/"] = map[string]interface{}{
   415  		"handler": "setup",
   416  	}
   417  
   418  	m["/status/"] = map[string]interface{}{
   419  		"handler": "status",
   420  	}
   421  
   422  	if params.shareHandlerPath != "" {
   423  		m[params.shareHandlerPath] = map[string]interface{}{
   424  			"handler": "share",
   425  			"handlerArgs": map[string]interface{}{
   426  				"blobRoot": "/bs/",
   427  			},
   428  		}
   429  	}
   430  
   431  	m["/sighelper/"] = map[string]interface{}{
   432  		"handler": "jsonsign",
   433  		"handlerArgs": map[string]interface{}{
   434  			"secretRing":    params.secretRing,
   435  			"keyId":         params.keyId,
   436  			"publicKeyDest": pubKeyDest,
   437  		},
   438  	}
   439  
   440  	storageType := "filesystem"
   441  	if params.packBlobs {
   442  		storageType = "diskpacked"
   443  	}
   444  	if params.blobPath != "" {
   445  		m["/bs/"] = map[string]interface{}{
   446  			"handler": "storage-" + storageType,
   447  			"handlerArgs": map[string]interface{}{
   448  				"path": params.blobPath,
   449  			},
   450  		}
   451  
   452  		m["/cache/"] = map[string]interface{}{
   453  			"handler": "storage-" + storageType,
   454  			"handlerArgs": map[string]interface{}{
   455  				"path": filepath.Join(params.blobPath, "/cache"),
   456  			},
   457  		}
   458  	}
   459  
   460  	if params.flickr != "" {
   461  		m["/importer-flickr/"] = map[string]interface{}{
   462  			"handler": "importer-flickr",
   463  			"handlerArgs": map[string]interface{}{
   464  				"apiKey": params.flickr,
   465  			},
   466  		}
   467  	}
   468  
   469  	if haveIndex {
   470  		syncArgs := map[string]interface{}{
   471  			"from": "/bs/",
   472  			"to":   params.indexerPath,
   473  		}
   474  		// TODO(mpl): Brad says the cond should be dest == /index-*.
   475  		// But what about when dest is index-mem and we have a local disk;
   476  		// don't we want to have an active synchandler to do the fullSyncOnStart?
   477  		// Anyway, that condition works for now.
   478  		if params.blobPath == "" {
   479  			// When our primary blob store is remote (s3 or google cloud),
   480  			// i.e not an efficient replication source, we do not want the
   481  			// synchandler to mirror to the indexer. But we still want a
   482  			// synchandler to provide the discovery for e.g tools like
   483  			// camtool sync. See http://camlistore.org/issue/201
   484  			syncArgs["idle"] = true
   485  		} else {
   486  			syncArgs["queue"] = map[string]interface{}{
   487  				"type": "kv",
   488  				"file": filepath.Join(params.blobPath, "sync-to-index-queue.kv"),
   489  			}
   490  		}
   491  		m["/sync/"] = map[string]interface{}{
   492  			"handler":     "sync",
   493  			"handlerArgs": syncArgs,
   494  		}
   495  
   496  		m["/bs-and-index/"] = map[string]interface{}{
   497  			"handler": "storage-replica",
   498  			"handlerArgs": map[string]interface{}{
   499  				"backends": []interface{}{"/bs/", params.indexerPath},
   500  			},
   501  		}
   502  
   503  		m["/bs-and-maybe-also-index/"] = map[string]interface{}{
   504  			"handler": "storage-cond",
   505  			"handlerArgs": map[string]interface{}{
   506  				"write": map[string]interface{}{
   507  					"if":   "isSchema",
   508  					"then": "/bs-and-index/",
   509  					"else": "/bs/",
   510  				},
   511  				"read": "/bs/",
   512  			},
   513  		}
   514  
   515  		searchArgs := map[string]interface{}{
   516  			"index": params.indexerPath,
   517  			"owner": params.searchOwner.String(),
   518  		}
   519  		if params.memoryIndex {
   520  			searchArgs["slurpToMemory"] = true
   521  		}
   522  		m["/my-search/"] = map[string]interface{}{
   523  			"handler":     "search",
   524  			"handlerArgs": searchArgs,
   525  		}
   526  	}
   527  
   528  	return
   529  }
   530  
   531  // genLowLevelConfig returns a low-level config from a high-level config.
   532  func genLowLevelConfig(conf *Config) (lowLevelConf *Config, err error) {
   533  	var (
   534  		baseURL    = conf.OptionalString("baseURL", "")
   535  		listen     = conf.OptionalString("listen", "")
   536  		auth       = conf.RequiredString("auth")
   537  		keyId      = conf.RequiredString("identity")
   538  		secretRing = conf.RequiredString("identitySecretRing")
   539  		tlsOn      = conf.OptionalBool("https", false)
   540  		tlsCert    = conf.OptionalString("HTTPSCertFile", "")
   541  		tlsKey     = conf.OptionalString("HTTPSKeyFile", "")
   542  
   543  		// Blob storage options
   544  		blobPath           = conf.OptionalString("blobPath", "")
   545  		packBlobs          = conf.OptionalBool("packBlobs", false)         // use diskpacked instead of the default filestorage
   546  		s3                 = conf.OptionalString("s3", "")                 // "access_key_id:secret_access_key:bucket[:hostname]"
   547  		googlecloudstorage = conf.OptionalString("googlecloudstorage", "") // "clientId:clientSecret:refreshToken:bucket"
   548  		googledrive        = conf.OptionalString("googledrive", "")        // "clientId:clientSecret:refreshToken:parentId"
   549  		// Enable the share handler. If true, and shareHandlerPath is empty,
   550  		// then shareHandlerPath defaults to "/share/".
   551  		shareHandler = conf.OptionalBool("shareHandler", false)
   552  		// URL prefix for the share handler. If set, overrides shareHandler.
   553  		shareHandlerPath = conf.OptionalString("shareHandlerPath", "")
   554  
   555  		// Index options
   556  		memoryIndex = conf.OptionalBool("memoryIndex", true) // copy disk-based index to memory on start-up
   557  		runIndex    = conf.OptionalBool("runIndex", true)    // if false: no search, no UI, etc.
   558  		dbname      = conf.OptionalString("dbname", "")      // for mysql, postgres, mongo
   559  		mysql       = conf.OptionalString("mysql", "")
   560  		postgres    = conf.OptionalString("postgres", "")
   561  		mongo       = conf.OptionalString("mongo", "")
   562  		sqliteFile  = conf.OptionalString("sqlite", "")
   563  		kvFile      = conf.OptionalString("kvIndexFile", "")
   564  
   565  		// Importer options
   566  		flickr = conf.OptionalString("flickr", "")
   567  
   568  		_       = conf.OptionalList("replicateTo")
   569  		publish = conf.OptionalObject("publish")
   570  		// alternative source tree, to override the embedded ui and/or closure resources.
   571  		// If non empty, the ui files will be expected at
   572  		// sourceRoot + "/server/camlistored/ui" and the closure library at
   573  		// sourceRoot + "/third_party/closure/lib"
   574  		// Also used by the publish handler.
   575  		sourceRoot = conf.OptionalString("sourceRoot", "")
   576  
   577  		ownerName = conf.OptionalString("ownerName", "")
   578  	)
   579  	if err := conf.Validate(); err != nil {
   580  		return nil, err
   581  	}
   582  
   583  	obj := jsonconfig.Obj{}
   584  	if tlsOn {
   585  		if (tlsCert != "") != (tlsKey != "") {
   586  			return nil, errors.New("Must set both TLSCertFile and TLSKeyFile (or neither to generate a self-signed cert)")
   587  		}
   588  		if tlsCert != "" {
   589  			obj["TLSCertFile"] = tlsCert
   590  			obj["TLSKeyFile"] = tlsKey
   591  		} else {
   592  			obj["TLSCertFile"] = osutil.DefaultTLSCert()
   593  			obj["TLSKeyFile"] = osutil.DefaultTLSKey()
   594  		}
   595  	}
   596  
   597  	if baseURL != "" {
   598  		u, err := url.Parse(baseURL)
   599  		if err != nil {
   600  			return nil, fmt.Errorf("Error parsing baseURL %q as a URL: %v", baseURL, err)
   601  		}
   602  		if u.Path != "" && u.Path != "/" {
   603  			return nil, fmt.Errorf("baseURL can't have a path, only a scheme, host, and optional port.")
   604  		}
   605  		u.Path = ""
   606  		obj["baseURL"] = u.String()
   607  	}
   608  	if listen != "" {
   609  		obj["listen"] = listen
   610  	}
   611  	obj["https"] = tlsOn
   612  	obj["auth"] = auth
   613  
   614  	username := ""
   615  	if dbname == "" {
   616  		username = osutil.Username()
   617  		if username == "" {
   618  			return nil, fmt.Errorf("USER (USERNAME on windows) env var not set; needed to define dbname")
   619  		}
   620  		dbname = "camli" + username
   621  	}
   622  
   623  	var indexerPath string
   624  	numIndexers := numSet(mongo, mysql, postgres, sqliteFile, kvFile)
   625  	switch {
   626  	case runIndex && numIndexers == 0:
   627  		return nil, fmt.Errorf("Unless runIndex is set to false, you must specify an index option (kvIndexFile, mongo, mysql, postgres, sqlite).")
   628  	case runIndex && numIndexers != 1:
   629  		return nil, fmt.Errorf("With runIndex set true, you can only pick exactly one indexer (mongo, mysql, postgres, sqlite).")
   630  	case !runIndex && numIndexers != 0:
   631  		return nil, fmt.Errorf("With runIndex disabled, you can't specify any of mongo, mysql, postgres, sqlite.")
   632  	case mysql != "":
   633  		indexerPath = "/index-mysql/"
   634  	case postgres != "":
   635  		indexerPath = "/index-postgres/"
   636  	case mongo != "":
   637  		indexerPath = "/index-mongo/"
   638  	case sqliteFile != "":
   639  		indexerPath = "/index-sqlite/"
   640  	case kvFile != "":
   641  		indexerPath = "/index-kv/"
   642  	}
   643  
   644  	entity, err := jsonsign.EntityFromSecring(keyId, secretRing)
   645  	if err != nil {
   646  		return nil, err
   647  	}
   648  	armoredPublicKey, err := jsonsign.ArmoredPublicKey(entity)
   649  	if err != nil {
   650  		return nil, err
   651  	}
   652  
   653  	nolocaldisk := blobPath == ""
   654  	if nolocaldisk {
   655  		if s3 == "" && googlecloudstorage == "" {
   656  			return nil, errors.New("You need at least one of blobPath (for localdisk) or s3 or googlecloudstorage configured for a blobserver.")
   657  		}
   658  		if s3 != "" && googlecloudstorage != "" {
   659  			return nil, errors.New("Using S3 as a primary storage and Google Cloud Storage as a mirror is not supported for now.")
   660  		}
   661  	}
   662  
   663  	if shareHandler && shareHandlerPath == "" {
   664  		shareHandlerPath = "/share/"
   665  	}
   666  
   667  	prefixesParams := &configPrefixesParams{
   668  		secretRing:       secretRing,
   669  		keyId:            keyId,
   670  		indexerPath:      indexerPath,
   671  		blobPath:         blobPath,
   672  		packBlobs:        packBlobs,
   673  		searchOwner:      blob.SHA1FromString(armoredPublicKey),
   674  		shareHandlerPath: shareHandlerPath,
   675  		flickr:           flickr,
   676  		memoryIndex:      memoryIndex,
   677  	}
   678  
   679  	prefixes := genLowLevelPrefixes(prefixesParams, ownerName)
   680  	var cacheDir string
   681  	if nolocaldisk {
   682  		// Whether camlistored is run from EC2 or not, we use
   683  		// a temp dir as the cache when primary storage is S3.
   684  		// TODO(mpl): s3CacheBucket
   685  		// See http://code.google.com/p/camlistore/issues/detail?id=85
   686  		cacheDir = filepath.Join(tempDir(), "camli-cache")
   687  	} else {
   688  		cacheDir = filepath.Join(blobPath, "cache")
   689  	}
   690  	if !noMkdir {
   691  		if err := os.MkdirAll(cacheDir, 0700); err != nil {
   692  			return nil, fmt.Errorf("Could not create blobs cache dir %s: %v", cacheDir, err)
   693  		}
   694  	}
   695  
   696  	published := []interface{}{}
   697  	if len(publish) > 0 {
   698  		if !runIndex {
   699  			return nil, fmt.Errorf("publishing requires an index")
   700  		}
   701  		published, err = addPublishedConfig(prefixes, publish, sourceRoot)
   702  		if err != nil {
   703  			return nil, fmt.Errorf("Could not generate config for published: %v", err)
   704  		}
   705  	}
   706  
   707  	if runIndex {
   708  		addUIConfig(prefixesParams, prefixes, "/ui/", published, sourceRoot)
   709  	}
   710  
   711  	if mysql != "" {
   712  		addMySQLConfig(prefixes, dbname, mysql)
   713  	}
   714  	if postgres != "" {
   715  		addPostgresConfig(prefixes, dbname, postgres)
   716  	}
   717  	if mongo != "" {
   718  		addMongoConfig(prefixes, dbname, mongo)
   719  	}
   720  	if sqliteFile != "" {
   721  		addSQLiteConfig(prefixes, sqliteFile)
   722  	}
   723  	if kvFile != "" {
   724  		addKVConfig(prefixes, kvFile)
   725  	}
   726  	if s3 != "" {
   727  		if err := addS3Config(prefixesParams, prefixes, s3); err != nil {
   728  			return nil, err
   729  		}
   730  	}
   731  	if googledrive != "" {
   732  		if err := addGoogleDriveConfig(prefixes, googledrive); err != nil {
   733  			return nil, err
   734  		}
   735  	}
   736  	if googlecloudstorage != "" {
   737  		if err := addGoogleCloudStorageConfig(prefixes, googlecloudstorage); err != nil {
   738  			return nil, err
   739  		}
   740  	}
   741  
   742  	obj["prefixes"] = (map[string]interface{})(prefixes)
   743  
   744  	lowLevelConf = &Config{
   745  		Obj:        obj,
   746  		configPath: conf.configPath,
   747  	}
   748  	return lowLevelConf, nil
   749  }
   750  
   751  func numSet(vv ...interface{}) (num int) {
   752  	for _, vi := range vv {
   753  		switch v := vi.(type) {
   754  		case string:
   755  			if v != "" {
   756  				num++
   757  			}
   758  		case bool:
   759  			if v {
   760  				num++
   761  			}
   762  		default:
   763  			panic("unknown type")
   764  		}
   765  	}
   766  	return
   767  }
   768  
   769  func setMap(m map[string]interface{}, v ...interface{}) {
   770  	if len(v) < 2 {
   771  		panic("too few args")
   772  	}
   773  	if len(v) == 2 {
   774  		m[v[0].(string)] = v[1]
   775  		return
   776  	}
   777  	setMap(m[v[0].(string)].(map[string]interface{}), v[1:]...)
   778  }