Home Download Docs Code Community
     1	/*
     2	Copyright 2012 The Perkeep Authors
     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 serverinit
    18	
    19	import (
    20		"encoding/json"
    21		"errors"
    22		"fmt"
    23		"log"
    24		"net/url"
    25		"os"
    26		"path"
    27		"path/filepath"
    28		"strconv"
    29		"strings"
    30	
    31		"go4.org/jsonconfig"
    32		"perkeep.org/internal/osutil"
    33		"perkeep.org/pkg/jsonsign"
    34		"perkeep.org/pkg/sorted"
    35		"perkeep.org/pkg/types/serverconfig"
    36	
    37		"go4.org/wkfs"
    38	)
    39	
    40	var (
    41		tempDir = os.TempDir
    42		noMkdir bool // for tests to not call os.Mkdir
    43	)
    44	
    45	type tlsOpts struct {
    46		autoCert  bool // use Perkeep's Let's Encrypt cache. but httpsCert takes precedence, if set.
    47		httpsCert string
    48		httpsKey  string
    49	}
    50	
    51	// genLowLevelConfig returns a low-level config from a high-level config.
    52	func genLowLevelConfig(conf *serverconfig.Config) (lowLevelConf *Config, err error) {
    53		b := &lowBuilder{
    54			high: conf,
    55			low: jsonconfig.Obj{
    56				"prefixes": make(map[string]interface{}),
    57			},
    58		}
    59		return b.build()
    60	}
    61	
    62	// A lowBuilder builds a low-level config from a high-level config.
    63	type lowBuilder struct {
    64		high *serverconfig.Config // high-level config (input)
    65		low  jsonconfig.Obj       // low-level handler config (output)
    66	}
    67	
    68	// args is an alias for map[string]interface{} just to cut down on
    69	// noise below.  But we take care to convert it back to
    70	// map[string]interface{} in the one place where we accept it.
    71	type args map[string]interface{}
    72	
    73	func (b *lowBuilder) addPrefix(at, handler string, a args) {
    74		v := map[string]interface{}{
    75			"handler": handler,
    76		}
    77		if a != nil {
    78			v["handlerArgs"] = (map[string]interface{})(a)
    79		}
    80		b.low["prefixes"].(map[string]interface{})[at] = v
    81	}
    82	
    83	func (b *lowBuilder) hasPrefix(p string) bool {
    84		_, ok := b.low["prefixes"].(map[string]interface{})[p]
    85		return ok
    86	}
    87	
    88	func (b *lowBuilder) runIndex() bool          { return b.high.RunIndex.Get() }
    89	func (b *lowBuilder) copyIndexToMemory() bool { return b.high.CopyIndexToMemory.Get() }
    90	
    91	type dbname string
    92	
    93	// possible arguments to dbName
    94	const (
    95		dbIndex           dbname = "index"
    96		dbBlobpackedIndex dbname = "blobpacked-index"
    97		dbDiskpackedIndex dbname = "diskpacked-index"
    98		dbUIThumbcache    dbname = "ui-thumbcache"
    99		dbSyncQueue       dbname = "queue-sync-to-" // only a prefix. the last part is the sync destination, e.g. "index".
   100	)
   101	
   102	// dbUnique returns the uniqueness string that is used in databases names to
   103	// differentiate them from databases used by other Perkeep instances on the same
   104	// DBMS.
   105	func (b *lowBuilder) dbUnique() string {
   106		if b.high.DBUnique != "" {
   107			return b.high.DBUnique
   108		}
   109		if b.high.Identity != "" {
   110			return strings.ToLower(b.high.Identity)
   111		}
   112		return osutil.Username() // may be empty, if $USER unset
   113	}
   114	
   115	// dbName returns which database to use for the provided user ("of"), which can
   116	// only be one of the const defined above. Returned values all follow the same name
   117	// scheme for consistency:
   118	// -prefixed with "pk_", so as to distinguish them from databases for other programs
   119	// -followed by a username-based uniqueness string
   120	// -last part says which component/part of perkeep it is about
   121	func (b *lowBuilder) dbName(of dbname) string {
   122		unique := b.dbUnique()
   123		if unique == "" {
   124			log.Printf("Could not define uniqueness for database of %q. Do not use the same index DBMS with other Perkeep instances.", of)
   125		}
   126		if unique == useDBNamesConfig {
   127			// this is the hint that we should revert to the old style DBNames, so this
   128			// instance can reuse its existing databases
   129			return b.oldDBNames(of)
   130		}
   131		prefix := "pk_"
   132		if unique != "" {
   133			prefix += unique + "_"
   134		}
   135		switch of {
   136		case dbIndex:
   137			if b.high.DBName != "" {
   138				return b.high.DBName
   139			}
   140			return prefix + "index"
   141		case dbBlobpackedIndex:
   142			return prefix + "blobpacked"
   143		case dbDiskpackedIndex:
   144			return prefix + "diskpacked"
   145		case dbUIThumbcache:
   146			return prefix + "uithumbmeta"
   147		}
   148		asString := string(of)
   149		if strings.HasPrefix(asString, string(dbSyncQueue)) {
   150			return prefix + "syncto_" + strings.TrimPrefix(asString, string(dbSyncQueue))
   151		}
   152		return ""
   153	}
   154	
   155	// As of rev 7eda9fd5027fda88166d6c03b6490cffbf2de5fb, we changed how the
   156	// databases names were defined. But we wanted the existing GCE instances to keep
   157	// on working with the old names, so that nothing would break for existing users,
   158	// without any intervention needed. Through the help of the perkeep-config-version
   159	// variable, set by the GCE launcher, we can know whether an instance is such an
   160	// "old" one, and in that case we keep on using the old database names. oldDBNames
   161	// returns these names.
   162	func (b *lowBuilder) oldDBNames(of dbname) string {
   163		switch of {
   164		case dbIndex:
   165			return "camlistore_index"
   166		case dbBlobpackedIndex:
   167			return "blobpacked_index"
   168		case "queue-sync-to-index":
   169			return "sync_index_queue"
   170		case dbUIThumbcache:
   171			return "ui_thumbmeta_cache"
   172		}
   173		return ""
   174	}
   175	
   176	var errNoOwner = errors.New("no owner")
   177	
   178	// Error is errNoOwner if no identity configured
   179	func (b *lowBuilder) searchOwner() (owner *serverconfig.Owner, err error) {
   180		if b.high.Identity == "" {
   181			return nil, errNoOwner
   182		}
   183		if b.high.IdentitySecretRing == "" {
   184			return nil, errNoOwner
   185		}
   186		return &serverconfig.Owner{
   187			Identity:    b.high.Identity,
   188			SecringFile: b.high.IdentitySecretRing,
   189		}, nil
   190	}
   191	
   192	// longIdentity returns the long form (16 chars) of the GPG key ID, in case the
   193	// user provided the short form (8 chars) in the config.
   194	func (b *lowBuilder) longIdentity() (string, error) {
   195		if b.high.Identity == "" {
   196			return "", errNoOwner
   197		}
   198		if strings.ToUpper(b.high.Identity) != b.high.Identity {
   199			return "", fmt.Errorf("identity %q is not all upper-case", b.high.Identity)
   200		}
   201		if len(b.high.Identity) == 16 {
   202			return b.high.Identity, nil
   203		}
   204		if b.high.IdentitySecretRing == "" {
   205			return "", errNoOwner
   206		}
   207		keyID, err := jsonsign.KeyIdFromRing(b.high.IdentitySecretRing)
   208		if err != nil {
   209			return "", fmt.Errorf("could not find any keyID in file %q: %v", b.high.IdentitySecretRing, err)
   210		}
   211		if !strings.HasSuffix(keyID, b.high.Identity) {
   212			return "", fmt.Errorf("%q identity not found in secret ring %v", b.high.Identity, b.high.IdentitySecretRing)
   213		}
   214		return keyID, nil
   215	}
   216	
   217	func addAppConfig(config map[string]interface{}, appConfig *serverconfig.App, low jsonconfig.Obj) {
   218		if appConfig.Listen != "" {
   219			config["listen"] = appConfig.Listen
   220		}
   221		if appConfig.APIHost != "" {
   222			config["apiHost"] = appConfig.APIHost
   223		}
   224		if appConfig.BackendURL != "" {
   225			config["backendURL"] = appConfig.BackendURL
   226		}
   227		if low["listen"] != nil && low["listen"].(string) != "" {
   228			config["serverListen"] = low["listen"].(string)
   229		}
   230		if low["baseURL"] != nil && low["baseURL"].(string) != "" {
   231			config["serverBaseURL"] = low["baseURL"].(string)
   232		}
   233	}
   234	
   235	func (b *lowBuilder) addPublishedConfig(tlsO *tlsOpts) error {
   236		published := b.high.Publish
   237		for k, v := range published {
   238			// trick in case all of the fields of v.App were omitted, which would leave v.App nil.
   239			if v.App == nil {
   240				v.App = &serverconfig.App{}
   241			}
   242			if v.CamliRoot == "" {
   243				return fmt.Errorf("missing \"camliRoot\" key in configuration for %s", k)
   244			}
   245			if v.GoTemplate == "" {
   246				return fmt.Errorf("missing \"goTemplate\" key in configuration for %s", k)
   247			}
   248			appConfig := map[string]interface{}{
   249				"camliRoot":  v.CamliRoot,
   250				"cacheRoot":  v.CacheRoot,
   251				"goTemplate": v.GoTemplate,
   252			}
   253			if v.SourceRoot != "" {
   254				appConfig["sourceRoot"] = v.SourceRoot
   255			}
   256			if v.HTTPSCert != "" && v.HTTPSKey != "" {
   257				// user can specify these directly in the publish section
   258				appConfig["httpsCert"] = v.HTTPSCert
   259				appConfig["httpsKey"] = v.HTTPSKey
   260			} else {
   261				// default to Perkeep parameters, if any
   262				if tlsO != nil {
   263					if tlsO.autoCert {
   264						appConfig["certManager"] = tlsO.autoCert
   265					}
   266					if tlsO.httpsCert != "" {
   267						appConfig["httpsCert"] = tlsO.httpsCert
   268					}
   269					if tlsO.httpsKey != "" {
   270						appConfig["httpsKey"] = tlsO.httpsKey
   271					}
   272				}
   273			}
   274			program := "publisher"
   275			if v.Program != "" {
   276				program = v.Program
   277			}
   278			a := args{
   279				"prefix":    k,
   280				"program":   program,
   281				"appConfig": appConfig,
   282			}
   283			addAppConfig(a, v.App, b.low)
   284			b.addPrefix(k, "app", a)
   285		}
   286		return nil
   287	}
   288	
   289	func (b *lowBuilder) addScanCabConfig(tlsO *tlsOpts) error {
   290		if b.high.ScanCab == nil {
   291			return nil
   292		}
   293		scancab := b.high.ScanCab
   294		if scancab.App == nil {
   295			scancab.App = &serverconfig.App{}
   296		}
   297		if scancab.Prefix == "" {
   298			return errors.New("missing \"prefix\" key in configuration for scanning cabinet")
   299		}
   300	
   301		program := "scanningcabinet"
   302		if scancab.Program != "" {
   303			program = scancab.Program
   304		}
   305	
   306		auth := scancab.Auth
   307		if auth == "" {
   308			auth = b.high.Auth
   309		}
   310		appConfig := map[string]interface{}{
   311			"auth": auth,
   312		}
   313		if scancab.HTTPSCert != "" && scancab.HTTPSKey != "" {
   314			appConfig["httpsCert"] = scancab.HTTPSCert
   315			appConfig["httpsKey"] = scancab.HTTPSKey
   316		} else {
   317			// default to Perkeep parameters, if any
   318			if tlsO != nil {
   319				appConfig["httpsCert"] = tlsO.httpsCert
   320				appConfig["httpsKey"] = tlsO.httpsKey
   321			}
   322		}
   323		a := args{
   324			"prefix":    scancab.Prefix,
   325			"program":   program,
   326			"appConfig": appConfig,
   327		}
   328		addAppConfig(a, scancab.App, b.low)
   329		b.addPrefix(scancab.Prefix, "app", a)
   330		return nil
   331	}
   332	
   333	func (b *lowBuilder) sortedName() string {
   334		switch {
   335		case b.high.MySQL != "":
   336			return "MySQL"
   337		case b.high.PostgreSQL != "":
   338			return "PostgreSQL"
   339		case b.high.Mongo != "":
   340			return "MongoDB"
   341		case b.high.MemoryIndex:
   342			return "in memory LevelDB"
   343		case b.high.SQLite != "":
   344			return "SQLite"
   345		case b.high.KVFile != "":
   346			return "cznic/kv"
   347		case b.high.LevelDB != "":
   348			return "LevelDB"
   349		}
   350		panic("internal error: sortedName didn't find a sorted implementation")
   351	}
   352	
   353	// kvFileType returns the file based sorted type defined for index storage, if
   354	// any. It defaults to "leveldb" otherwise.
   355	func (b *lowBuilder) kvFileType() string {
   356		switch {
   357		case b.high.SQLite != "":
   358			return "sqlite"
   359		case b.high.KVFile != "":
   360			return "kv"
   361		case b.high.LevelDB != "":
   362			return "leveldb"
   363		default:
   364			return sorted.DefaultKVFileType
   365		}
   366	}
   367	
   368	func (b *lowBuilder) addUIConfig() {
   369		args := map[string]interface{}{
   370			"cache": "/cache/",
   371		}
   372		if b.high.SourceRoot != "" {
   373			args["sourceRoot"] = b.high.SourceRoot
   374		}
   375		var thumbCache map[string]interface{}
   376		if b.high.BlobPath != "" {
   377			thumbCache = map[string]interface{}{
   378				"type": b.kvFileType(),
   379				"file": filepath.Join(b.high.BlobPath, "thumbmeta."+b.kvFileType()),
   380			}
   381		}
   382		if thumbCache == nil {
   383			sorted, err := b.sortedStorage(dbUIThumbcache)
   384			if err == nil {
   385				thumbCache = sorted
   386			}
   387		}
   388		if thumbCache != nil {
   389			args["scaledImage"] = thumbCache
   390		}
   391		b.addPrefix("/ui/", "ui", args)
   392	}
   393	
   394	func (b *lowBuilder) mongoIndexStorage(confStr string, sortedType dbname) (map[string]interface{}, error) {
   395		dbName := b.dbName(sortedType)
   396		if dbName == "" {
   397			return nil, fmt.Errorf("no database name configured for sorted store %q", sortedType)
   398		}
   399		fields := strings.Split(confStr, "@")
   400		if len(fields) == 2 {
   401			host := fields[1]
   402			fields = strings.Split(fields[0], ":")
   403			if len(fields) == 2 {
   404				user, pass := fields[0], fields[1]
   405				return map[string]interface{}{
   406					"type":     "mongo",
   407					"host":     host,
   408					"user":     user,
   409					"password": pass,
   410					"database": dbName,
   411				}, nil
   412			}
   413		}
   414		return nil, errors.New("Malformed mongo config string; want form: \"user:password@host\"")
   415	}
   416	
   417	// parses "user@host:password", which you think would be easy, but we
   418	// documented this format without thinking about port numbers, so this
   419	// uses heuristics to guess what extra colons mean.
   420	func parseUserHostPass(v string) (user, host, password string, ok bool) {
   421		f := strings.SplitN(v, "@", 2)
   422		if len(f) != 2 {
   423			return
   424		}
   425		user = f[0]
   426		f = strings.Split(f[1], ":")
   427		if len(f) < 2 {
   428			return "", "", "", false
   429		}
   430		host = f[0]
   431		f = f[1:]
   432		if len(f) >= 2 {
   433			if _, err := strconv.ParseUint(f[0], 10, 16); err == nil {
   434				host = host + ":" + f[0]
   435				f = f[1:]
   436			}
   437		}
   438		password = strings.Join(f, ":")
   439		ok = true
   440		return
   441	}
   442	
   443	func (b *lowBuilder) dbIndexStorage(rdbms, confStr string, sortedType dbname) (map[string]interface{}, error) {
   444		dbName := b.dbName(sortedType)
   445		if dbName == "" {
   446			return nil, fmt.Errorf("no database name configured for sorted store %q", sortedType)
   447		}
   448		user, host, password, ok := parseUserHostPass(confStr)
   449		if !ok {
   450			return nil, fmt.Errorf("Malformed %s config string. Want: \"user@host:password\"", rdbms)
   451		}
   452		return map[string]interface{}{
   453			"type":     rdbms,
   454			"host":     host,
   455			"user":     user,
   456			"password": password,
   457			"database": dbName,
   458		}, nil
   459	}
   460	
   461	func (b *lowBuilder) sortedStorage(sortedType dbname) (map[string]interface{}, error) {
   462		return b.sortedStorageAt(sortedType, "")
   463	}
   464	
   465	// sortedDBMS returns the configuration for a name database on one of the
   466	// DBMS, if any was found in the configuration. It returns nil otherwise.
   467	func (b *lowBuilder) sortedDBMS(named dbname) (map[string]interface{}, error) {
   468		if b.high.MySQL != "" {
   469			return b.dbIndexStorage("mysql", b.high.MySQL, named)
   470		}
   471		if b.high.PostgreSQL != "" {
   472			return b.dbIndexStorage("postgres", b.high.PostgreSQL, named)
   473		}
   474		if b.high.Mongo != "" {
   475			return b.mongoIndexStorage(b.high.Mongo, named)
   476		}
   477		return nil, nil
   478	}
   479	
   480	// filePrefix gives a file path of where to put the database. It can be omitted by
   481	// some sorted implementations, but is required by others.
   482	// The filePrefix should be to a file, not a directory, and should not end in a ".ext" extension.
   483	// An extension like ".kv" or ".sqlite" will be added.
   484	func (b *lowBuilder) sortedStorageAt(sortedType dbname, filePrefix string) (map[string]interface{}, error) {
   485		dbms, err := b.sortedDBMS(sortedType)
   486		if err != nil {
   487			return nil, err
   488		}
   489		if dbms != nil {
   490			return dbms, nil
   491		}
   492		if b.high.MemoryIndex {
   493			return map[string]interface{}{
   494				"type": "memory",
   495			}, nil
   496		}
   497		if sortedType != "index" && filePrefix == "" {
   498			return nil, fmt.Errorf("internal error: use of sortedStorageAt with a non-index type (%v) and no file location for non-database sorted implementation", sortedType)
   499		}
   500		// dbFile returns path directly if sortedType == "index", else it returns filePrefix+"."+ext.
   501		dbFile := func(path, ext string) string {
   502			if sortedType == "index" {
   503				return path
   504			}
   505			return filePrefix + "." + ext
   506		}
   507		if b.high.SQLite != "" {
   508			return map[string]interface{}{
   509				"type": "sqlite",
   510				"file": dbFile(b.high.SQLite, "sqlite"),
   511			}, nil
   512		}
   513		if b.high.KVFile != "" {
   514			return map[string]interface{}{
   515				"type": "kv",
   516				"file": dbFile(b.high.KVFile, "kv"),
   517			}, nil
   518		}
   519		if b.high.LevelDB != "" {
   520			return map[string]interface{}{
   521				"type": "leveldb",
   522				"file": dbFile(b.high.LevelDB, "leveldb"),
   523			}, nil
   524		}
   525		panic("internal error: sortedStorageAt didn't find a sorted implementation")
   526	}
   527	
   528	func (b *lowBuilder) thatQueueUnlessMemory(thatQueue map[string]interface{}) (queue map[string]interface{}) {
   529		// TODO(mpl): what about if b.high.MemoryIndex ?
   530		if b.high.MemoryStorage {
   531			return map[string]interface{}{
   532				"type": "memory",
   533			}
   534		}
   535		return thatQueue
   536	}
   537	
   538	func (b *lowBuilder) addS3Config(s3 string) error {
   539		f := strings.SplitN(s3, ":", 4)
   540		if len(f) < 3 {
   541			return errors.New(`genconfig: expected "s3" field to be of form "access_key_id:secret_access_key:bucket[/optional/dir][:hostname]"`)
   542		}
   543		accessKey, secret, bucket := f[0], f[1], f[2]
   544		var hostname string
   545		if len(f) == 4 {
   546			hostname = f[3]
   547		}
   548		isReplica := b.hasPrefix("/bs/")
   549		s3Prefix := ""
   550		s3Args := args{
   551			"aws_access_key":        accessKey,
   552			"aws_secret_access_key": secret,
   553			"bucket":                bucket,
   554		}
   555		if hostname != "" {
   556			s3Args["hostname"] = hostname
   557		}
   558		if isReplica {
   559			s3Prefix = "/sto-s3/"
   560			b.addPrefix(s3Prefix, "storage-s3", s3Args)
   561			if b.high.BlobPath == "" && !b.high.MemoryStorage {
   562				panic("unexpected empty blobpath with sync-to-s3")
   563			}
   564			b.addPrefix("/sync-to-s3/", "sync", args{
   565				"from": "/bs/",
   566				"to":   s3Prefix,
   567				"queue": b.thatQueueUnlessMemory(
   568					map[string]interface{}{
   569						"type": b.kvFileType(),
   570						"file": filepath.Join(b.high.BlobPath, "sync-to-s3-queue."+b.kvFileType()),
   571					}),
   572			})
   573			return nil
   574		}
   575	
   576		// TODO(mpl): s3CacheBucket
   577		// See https://perkeep.org/issue/85
   578		b.addPrefix("/cache/", "storage-filesystem", args{
   579			"path": filepath.Join(tempDir(), "camli-cache"),
   580		})
   581	
   582		s3Prefix = "/bs/"
   583		if !b.high.PackRelated {
   584			b.addPrefix(s3Prefix, "storage-s3", s3Args)
   585			return nil
   586		}
   587		packedS3Args := func(bucket string) args {
   588			a := args{
   589				"bucket":                bucket,
   590				"aws_access_key":        accessKey,
   591				"aws_secret_access_key": secret,
   592			}
   593			if hostname != "" {
   594				a["hostname"] = hostname
   595			}
   596			return a
   597		}
   598	
   599		b.addPrefix("/bs-loose/", "storage-s3", packedS3Args(path.Join(bucket, "loose")))
   600		b.addPrefix("/bs-packed/", "storage-s3", packedS3Args(path.Join(bucket, "packed")))
   601	
   602		// If index is DBMS, then blobPackedIndex is in DBMS too.
   603		// Otherwise blobPackedIndex is same file-based DB as the index,
   604		// in same dir, but named packindex.dbtype.
   605		blobPackedIndex, err := b.sortedStorageAt(dbBlobpackedIndex, filepath.Join(b.indexFileDir(), "packindex"))
   606		if err != nil {
   607			return err
   608		}
   609		b.addPrefix(s3Prefix, "storage-blobpacked", args{
   610			"smallBlobs": "/bs-loose/",
   611			"largeBlobs": "/bs-packed/",
   612			"metaIndex":  blobPackedIndex,
   613		})
   614	
   615		return nil
   616	}
   617	
   618	func (b *lowBuilder) addB2Config(b2 string) error {
   619		f := strings.SplitN(b2, ":", 3)
   620		if len(f) < 3 {
   621			return errors.New(`genconfig: expected "b2" field to be of form "account_id:application_key:bucket[/optional/dir]"`)
   622		}
   623		account, key, bucket := f[0], f[1], f[2]
   624		isReplica := b.hasPrefix("/bs/")
   625		b2Prefix := ""
   626		b2Auth := map[string]interface{}{
   627			"account_id":      account,
   628			"application_key": key,
   629		}
   630		b2Args := args{
   631			"auth":   b2Auth,
   632			"bucket": bucket,
   633		}
   634		if isReplica {
   635			b2Prefix = "/sto-b2/"
   636			b.addPrefix(b2Prefix, "storage-b2", b2Args)
   637			if b.high.BlobPath == "" && !b.high.MemoryStorage {
   638				panic("unexpected empty blobpath with sync-to-b2")
   639			}
   640			b.addPrefix("/sync-to-b2/", "sync", args{
   641				"from": "/bs/",
   642				"to":   b2Prefix,
   643				"queue": b.thatQueueUnlessMemory(
   644					map[string]interface{}{
   645						"type": b.kvFileType(),
   646						"file": filepath.Join(b.high.BlobPath, "sync-to-b2-queue."+b.kvFileType()),
   647					}),
   648			})
   649			return nil
   650		}
   651	
   652		b.addPrefix("/cache/", "storage-filesystem", args{
   653			"path": filepath.Join(tempDir(), "camli-cache"),
   654		})
   655	
   656		b2Prefix = "/bs/"
   657		if !b.high.PackRelated {
   658			b.addPrefix(b2Prefix, "storage-b2", b2Args)
   659			return nil
   660		}
   661		packedB2Args := func(bucket string) args {
   662			a := args{
   663				"bucket": bucket,
   664				"auth": map[string]interface{}{
   665					"account_id":      account,
   666					"application_key": key,
   667				},
   668			}
   669			return a
   670		}
   671	
   672		b.addPrefix("/bs-loose/", "storage-b2", packedB2Args(path.Join(bucket, "loose")))
   673		b.addPrefix("/bs-packed/", "storage-b2", packedB2Args(path.Join(bucket, "packed")))
   674	
   675		// If index is DBMS, then blobPackedIndex is in DBMS too.
   676		// Otherwise blobPackedIndex is same file-based DB as the index,
   677		// in same dir, but named packindex.dbtype.
   678		blobPackedIndex, err := b.sortedStorageAt(dbBlobpackedIndex, filepath.Join(b.indexFileDir(), "packindex"))
   679		if err != nil {
   680			return err
   681		}
   682		b.addPrefix(b2Prefix, "storage-blobpacked", args{
   683			"smallBlobs": "/bs-loose/",
   684			"largeBlobs": "/bs-packed/",
   685			"metaIndex":  blobPackedIndex,
   686		})
   687	
   688		return nil
   689	}
   690	
   691	func (b *lowBuilder) addGoogleDriveConfig(v string) error {
   692		f := strings.SplitN(v, ":", 4)
   693		if len(f) != 4 {
   694			return errors.New(`genconfig: expected "googledrive" field to be of form "client_id:client_secret:refresh_token:parent_id"`)
   695		}
   696		clientId, secret, refreshToken, parentId := f[0], f[1], f[2], f[3]
   697	
   698		isPrimary := !b.hasPrefix("/bs/")
   699		prefix := ""
   700		if isPrimary {
   701			prefix = "/bs/"
   702			if b.high.PackRelated {
   703				return errors.New("TODO: finish packRelated support for Google Drive")
   704			}
   705		} else {
   706			prefix = "/sto-googledrive/"
   707		}
   708		b.addPrefix(prefix, "storage-googledrive", args{
   709			"parent_id": parentId,
   710			"auth": map[string]interface{}{
   711				"client_id":     clientId,
   712				"client_secret": secret,
   713				"refresh_token": refreshToken,
   714			},
   715		})
   716	
   717		if isPrimary {
   718			b.addPrefix("/cache/", "storage-filesystem", args{
   719				"path": filepath.Join(tempDir(), "camli-cache"),
   720			})
   721		} else {
   722			b.addPrefix("/sync-to-googledrive/", "sync", args{
   723				"from": "/bs/",
   724				"to":   prefix,
   725				"queue": b.thatQueueUnlessMemory(
   726					map[string]interface{}{
   727						"type": b.kvFileType(),
   728						"file": filepath.Join(b.high.BlobPath, "sync-to-googledrive-queue."+b.kvFileType()),
   729					}),
   730			})
   731		}
   732	
   733		return nil
   734	}
   735	
   736	var errGCSUsage = errors.New(`genconfig: expected "googlecloudstorage" field to be of form "client_id:client_secret:refresh_token:bucket[/dir/]" or ":bucketname[/dir/]"`)
   737	
   738	func (b *lowBuilder) addGoogleCloudStorageConfig(v string) error {
   739		var clientID, secret, refreshToken, bucket string
   740		f := strings.SplitN(v, ":", 4)
   741		switch len(f) {
   742		default:
   743			return errGCSUsage
   744		case 4:
   745			clientID, secret, refreshToken, bucket = f[0], f[1], f[2], f[3]
   746		case 2:
   747			if f[0] != "" {
   748				return errGCSUsage
   749			}
   750			bucket = f[1]
   751			clientID = "auto"
   752		}
   753	
   754		isReplica := b.hasPrefix("/bs/")
   755		if isReplica {
   756			gsPrefix := "/sto-googlecloudstorage/"
   757			b.addPrefix(gsPrefix, "storage-googlecloudstorage", args{
   758				"bucket": bucket,
   759				"auth": map[string]interface{}{
   760					"client_id":     clientID,
   761					"client_secret": secret,
   762					"refresh_token": refreshToken,
   763				},
   764			})
   765	
   766			b.addPrefix("/sync-to-googlecloudstorage/", "sync", args{
   767				"from": "/bs/",
   768				"to":   gsPrefix,
   769				"queue": b.thatQueueUnlessMemory(
   770					map[string]interface{}{
   771						"type": b.kvFileType(),
   772						"file": filepath.Join(b.high.BlobPath, "sync-to-googlecloud-queue."+b.kvFileType()),
   773					}),
   774			})
   775			return nil
   776		}
   777	
   778		// TODO: cacheBucket like s3CacheBucket?
   779		b.addPrefix("/cache/", "storage-filesystem", args{
   780			"path": filepath.Join(tempDir(), "camli-cache"),
   781		})
   782		if b.high.PackRelated {
   783			b.addPrefix("/bs-loose/", "storage-googlecloudstorage", args{
   784				"bucket": bucket + "/loose",
   785				"auth": map[string]interface{}{
   786					"client_id":     clientID,
   787					"client_secret": secret,
   788					"refresh_token": refreshToken,
   789				},
   790			})
   791			b.addPrefix("/bs-packed/", "storage-googlecloudstorage", args{
   792				"bucket": bucket + "/packed",
   793				"auth": map[string]interface{}{
   794					"client_id":     clientID,
   795					"client_secret": secret,
   796					"refresh_token": refreshToken,
   797				},
   798			})
   799			// If index is DBMS, then blobPackedIndex is in DBMS too.
   800			// Otherwise blobPackedIndex is same file-based DB as the index,
   801			// in same dir, but named packindex.dbtype.
   802			blobPackedIndex, err := b.sortedStorageAt(dbBlobpackedIndex, filepath.Join(b.indexFileDir(), "packindex"))
   803			if err != nil {
   804				return err
   805			}
   806			b.addPrefix("/bs/", "storage-blobpacked", args{
   807				"smallBlobs": "/bs-loose/",
   808				"largeBlobs": "/bs-packed/",
   809				"metaIndex":  blobPackedIndex,
   810			})
   811			return nil
   812		}
   813		b.addPrefix("/bs/", "storage-googlecloudstorage", args{
   814			"bucket": bucket,
   815			"auth": map[string]interface{}{
   816				"client_id":     clientID,
   817				"client_secret": secret,
   818				"refresh_token": refreshToken,
   819			},
   820		})
   821	
   822		return nil
   823	}
   824	
   825	// indexFileDir returns the directory of the sqlite or kv file, or the
   826	// empty string.
   827	func (b *lowBuilder) indexFileDir() string {
   828		switch {
   829		case b.high.SQLite != "":
   830			return filepath.Dir(b.high.SQLite)
   831		case b.high.KVFile != "":
   832			return filepath.Dir(b.high.KVFile)
   833		case b.high.LevelDB != "":
   834			return filepath.Dir(b.high.LevelDB)
   835		}
   836		return ""
   837	}
   838	
   839	func (b *lowBuilder) syncToIndexArgs() (map[string]interface{}, error) {
   840		a := map[string]interface{}{
   841			"from": "/bs/",
   842			"to":   "/index/",
   843		}
   844	
   845		// TODO(mpl): see if we want to have the same logic with all the other queues. probably.
   846		const sortedType = "queue-sync-to-index"
   847		if dbName := b.dbName(sortedType); dbName != "" {
   848			qj, err := b.sortedDBMS(sortedType)
   849			if err != nil {
   850				return nil, err
   851			}
   852			if qj == nil && b.high.MemoryIndex {
   853				qj = map[string]interface{}{
   854					"type": "memory",
   855				}
   856			}
   857			if qj != nil {
   858				// i.e. the index is configured on a DBMS, so we put the queue there too
   859				a["queue"] = qj
   860				return a, nil
   861			}
   862		}
   863	
   864		// TODO: currently when using s3, the index must be
   865		// sqlite or kvfile, since only through one of those
   866		// can we get a directory.
   867		if !b.high.MemoryStorage && b.high.BlobPath == "" && b.indexFileDir() == "" {
   868			// We don't actually have a working sync handler, but we keep a stub registered
   869			// so it can be referred to from other places.
   870			// See http://perkeep.org/issue/201
   871			a["idle"] = true
   872			return a, nil
   873		}
   874	
   875		dir := b.high.BlobPath
   876		if dir == "" {
   877			dir = b.indexFileDir()
   878		}
   879		a["queue"] = b.thatQueueUnlessMemory(
   880			map[string]interface{}{
   881				"type": b.kvFileType(),
   882				"file": filepath.Join(dir, "sync-to-index-queue."+b.kvFileType()),
   883			})
   884	
   885		return a, nil
   886	}
   887	
   888	func (b *lowBuilder) genLowLevelPrefixes() error {
   889		root := "/bs/"
   890		pubKeyDest := root
   891		if b.runIndex() {
   892			root = "/bs-and-maybe-also-index/"
   893			pubKeyDest = "/bs-and-index/"
   894		}
   895	
   896		rootArgs := map[string]interface{}{
   897			"stealth":      false,
   898			"blobRoot":     root,
   899			"helpRoot":     "/help/",
   900			"statusRoot":   "/status/",
   901			"jsonSignRoot": "/sighelper/",
   902		}
   903		if b.high.OwnerName != "" {
   904			rootArgs["ownerName"] = b.high.OwnerName
   905		}
   906		if b.runIndex() {
   907			rootArgs["searchRoot"] = "/my-search/"
   908		}
   909		if path := b.high.ShareHandlerPath; path != "" {
   910			rootArgs["shareRoot"] = path
   911			b.addPrefix(path, "share", args{
   912				"blobRoot": "/bs/",
   913				"index":    "/index/",
   914			})
   915		}
   916		b.addPrefix("/", "root", rootArgs)
   917		b.addPrefix("/setup/", "setup", nil)
   918		b.addPrefix("/status/", "status", nil)
   919		b.addPrefix("/help/", "help", nil)
   920	
   921		importerArgs := args{}
   922		if b.high.Flickr != "" {
   923			importerArgs["flickr"] = map[string]interface{}{
   924				"clientSecret": b.high.Flickr,
   925			}
   926		}
   927		if b.high.Picasa != "" {
   928			importerArgs["picasa"] = map[string]interface{}{
   929				"clientSecret": b.high.Picasa,
   930			}
   931		}
   932		if b.high.Instapaper != "" {
   933			importerArgs["instapaper"] = map[string]interface{}{
   934				"clientSecret": b.high.Instapaper,
   935			}
   936		}
   937		if b.runIndex() {
   938			b.addPrefix("/importer/", "importer", importerArgs)
   939		}
   940	
   941		b.addPrefix("/sighelper/", "jsonsign", args{
   942			"secretRing":    b.high.IdentitySecretRing,
   943			"keyId":         b.high.Identity,
   944			"publicKeyDest": pubKeyDest,
   945		})
   946	
   947		storageType := "filesystem"
   948		if b.high.PackBlobs {
   949			storageType = "diskpacked"
   950		}
   951		if b.high.BlobPath != "" {
   952			if b.high.PackRelated {
   953				b.addPrefix("/bs-loose/", "storage-filesystem", args{
   954					"path": b.high.BlobPath,
   955				})
   956				b.addPrefix("/bs-packed/", "storage-filesystem", args{
   957					"path": filepath.Join(b.high.BlobPath, "packed"),
   958				})
   959				blobPackedIndex, err := b.sortedStorageAt(dbBlobpackedIndex, filepath.Join(b.high.BlobPath, "packed", "packindex"))
   960				if err != nil {
   961					return err
   962				}
   963				b.addPrefix("/bs/", "storage-blobpacked", args{
   964					"smallBlobs": "/bs-loose/",
   965					"largeBlobs": "/bs-packed/",
   966					"metaIndex":  blobPackedIndex,
   967				})
   968			} else if b.high.PackBlobs {
   969				diskpackedIndex, err := b.sortedStorageAt(dbDiskpackedIndex, filepath.Join(b.high.BlobPath, "diskpacked-index"))
   970				if err != nil {
   971					return err
   972				}
   973				b.addPrefix("/bs/", "storage-"+storageType, args{
   974					"path":      b.high.BlobPath,
   975					"metaIndex": diskpackedIndex,
   976				})
   977			} else {
   978				b.addPrefix("/bs/", "storage-"+storageType, args{
   979					"path": b.high.BlobPath,
   980				})
   981			}
   982			if b.high.PackBlobs {
   983				b.addPrefix("/cache/", "storage-"+storageType, args{
   984					"path": filepath.Join(b.high.BlobPath, "/cache"),
   985					"metaIndex": map[string]interface{}{
   986						"type": b.kvFileType(),
   987						"file": filepath.Join(b.high.BlobPath, "cache", "index."+b.kvFileType()),
   988					},
   989				})
   990			} else {
   991				b.addPrefix("/cache/", "storage-"+storageType, args{
   992					"path": filepath.Join(b.high.BlobPath, "/cache"),
   993				})
   994			}
   995		} else if b.high.MemoryStorage {
   996			b.addPrefix("/bs/", "storage-memory", nil)
   997			b.addPrefix("/cache/", "storage-memory", nil)
   998		}
   999	
  1000		if b.runIndex() {
  1001			syncArgs, err := b.syncToIndexArgs()
  1002			if err != nil {
  1003				return err
  1004			}
  1005			b.addPrefix("/sync/", "sync", syncArgs)
  1006	
  1007			b.addPrefix("/bs-and-index/", "storage-replica", args{
  1008				"backends": []interface{}{"/bs/", "/index/"},
  1009			})
  1010	
  1011			b.addPrefix("/bs-and-maybe-also-index/", "storage-cond", args{
  1012				"write": map[string]interface{}{
  1013					"if":   "isSchema",
  1014					"then": "/bs-and-index/",
  1015					"else": "/bs/",
  1016				},
  1017				"read": "/bs/",
  1018			})
  1019	
  1020			owner, err := b.searchOwner()
  1021			if err != nil {
  1022				return err
  1023			}
  1024			searchArgs := args{
  1025				"index": "/index/",
  1026				"owner": map[string]interface{}{
  1027					"identity":    owner.Identity,
  1028					"secringFile": owner.SecringFile,
  1029				},
  1030			}
  1031			if b.copyIndexToMemory() {
  1032				searchArgs["slurpToMemory"] = true
  1033			}
  1034			b.addPrefix("/my-search/", "search", searchArgs)
  1035		}
  1036	
  1037		return nil
  1038	}
  1039	
  1040	func (b *lowBuilder) build() (*Config, error) {
  1041		conf, low := b.high, b.low
  1042		if conf.CamliNetIP != "" {
  1043			if !conf.HTTPS {
  1044				return nil, errors.New("CamliNetIP requires HTTPS")
  1045			}
  1046			if conf.HTTPSCert != "" || conf.HTTPSKey != "" || conf.Listen != "" || conf.BaseURL != "" {
  1047				return nil, errors.New("CamliNetIP is mutually exclusive with HTTPSCert, HTTPSKey, Listen, and BaseURL")
  1048			}
  1049			low["camliNetIP"] = conf.CamliNetIP
  1050		}
  1051		if conf.HTTPS {
  1052			if (conf.HTTPSCert != "") != (conf.HTTPSKey != "") {
  1053				return nil, errors.New("Must set both httpsCert and httpsKey (or neither to generate a self-signed cert)")
  1054			}
  1055			if conf.HTTPSCert != "" {
  1056				low["httpsCert"] = conf.HTTPSCert
  1057				low["httpsKey"] = conf.HTTPSKey
  1058			}
  1059		}
  1060	
  1061		if conf.BaseURL != "" {
  1062			u, err := url.Parse(conf.BaseURL)
  1063			if err != nil {
  1064				return nil, fmt.Errorf("Error parsing baseURL %q as a URL: %v", conf.BaseURL, err)
  1065			}
  1066			if u.Path != "" && u.Path != "/" {
  1067				return nil, fmt.Errorf("baseURL can't have a path, only a scheme, host, and optional port")
  1068			}
  1069			u.Path = ""
  1070			low["baseURL"] = u.String()
  1071		}
  1072		if conf.Listen != "" {
  1073			low["listen"] = conf.Listen
  1074		}
  1075		if conf.PackBlobs && conf.PackRelated {
  1076			return nil, errors.New("can't use both packBlobs (for 'diskpacked') and packRelated (for 'blobpacked')")
  1077		}
  1078		low["https"] = conf.HTTPS
  1079		low["auth"] = conf.Auth
  1080	
  1081		numIndexers := numSet(conf.LevelDB, conf.Mongo, conf.MySQL, conf.PostgreSQL, conf.SQLite, conf.KVFile, conf.MemoryIndex)
  1082	
  1083		switch {
  1084		case b.runIndex() && numIndexers == 0:
  1085			return nil, fmt.Errorf("Unless runIndex is set to false, you must specify an index option (kvIndexFile, leveldb, mongo, mysql, postgres, sqlite, memoryIndex).")
  1086		case b.runIndex() && numIndexers != 1:
  1087			return nil, fmt.Errorf("With runIndex set true, you can only pick exactly one indexer (mongo, mysql, postgres, sqlite, kvIndexFile, leveldb, memoryIndex).")
  1088		case !b.runIndex() && numIndexers != 0:
  1089			log.Printf("Indexer disabled, but %v will be used for other indexes, queues, caches, etc.", b.sortedName())
  1090		}
  1091	
  1092		longID, err := b.longIdentity()
  1093		if err != nil {
  1094			return nil, err
  1095		}
  1096		b.high.Identity = longID
  1097	
  1098		noLocalDisk := conf.BlobPath == ""
  1099		if noLocalDisk {
  1100			if !conf.MemoryStorage && conf.S3 == "" && conf.B2 == "" && conf.GoogleCloudStorage == "" {
  1101				return nil, errors.New("Unless memoryStorage is set, you must specify at least one storage option for your blobserver (blobPath (for localdisk), s3, b2, googlecloudstorage).")
  1102			}
  1103			if !conf.MemoryStorage && conf.S3 != "" && conf.GoogleCloudStorage != "" {
  1104				return nil, errors.New("Using S3 as a primary storage and Google Cloud Storage as a mirror is not supported for now.")
  1105			}
  1106			if !conf.MemoryStorage && conf.B2 != "" && conf.GoogleCloudStorage != "" {
  1107				return nil, errors.New("Using B2 as a primary storage and Google Cloud Storage as a mirror is not supported for now.")
  1108			}
  1109		}
  1110		if conf.ShareHandler && conf.ShareHandlerPath == "" {
  1111			conf.ShareHandlerPath = "/share/"
  1112		}
  1113		if conf.MemoryStorage {
  1114			noMkdir = true
  1115			if conf.BlobPath != "" {
  1116				return nil, errors.New("memoryStorage and blobPath are mutually exclusive.")
  1117			}
  1118			if conf.PackRelated {
  1119				return nil, errors.New("memoryStorage doesn't support packRelated.")
  1120			}
  1121		}
  1122	
  1123		if err := b.genLowLevelPrefixes(); err != nil {
  1124			return nil, err
  1125		}
  1126	
  1127		var cacheDir string
  1128		if noLocalDisk {
  1129			// Whether perkeepd is run from EC2 or not, we use
  1130			// a temp dir as the cache when primary storage is S3.
  1131			// TODO(mpl): s3CacheBucket
  1132			// See https://perkeep.org/issue/85
  1133			cacheDir = filepath.Join(tempDir(), "camli-cache")
  1134		} else {
  1135			cacheDir = filepath.Join(conf.BlobPath, "cache")
  1136		}
  1137		if !noMkdir {
  1138			if err := os.MkdirAll(cacheDir, 0700); err != nil {
  1139				return nil, fmt.Errorf("Could not create blobs cache dir %s: %v", cacheDir, err)
  1140			}
  1141		}
  1142	
  1143		if len(conf.Publish) > 0 {
  1144			if !b.runIndex() {
  1145				return nil, fmt.Errorf("publishing requires an index")
  1146			}
  1147			var tlsO *tlsOpts
  1148			httpsCert, ok1 := low["httpsCert"].(string)
  1149			httpsKey, ok2 := low["httpsKey"].(string)
  1150			if ok1 && ok2 {
  1151				tlsO = &tlsOpts{
  1152					httpsCert: httpsCert,
  1153					httpsKey:  httpsKey,
  1154				}
  1155			} else if conf.HTTPS {
  1156				tlsO = &tlsOpts{
  1157					autoCert: true,
  1158				}
  1159			}
  1160			if err := b.addPublishedConfig(tlsO); err != nil {
  1161				return nil, fmt.Errorf("Could not generate config for published: %v", err)
  1162			}
  1163		}
  1164	
  1165		if conf.ScanCab != nil {
  1166			if !b.runIndex() {
  1167				return nil, fmt.Errorf("scanning cabinet requires an index")
  1168			}
  1169			var tlsO *tlsOpts
  1170			httpsCert, ok1 := low["httpsCert"].(string)
  1171			httpsKey, ok2 := low["httpsKey"].(string)
  1172			if ok1 && ok2 {
  1173				tlsO = &tlsOpts{
  1174					httpsCert: httpsCert,
  1175					httpsKey:  httpsKey,
  1176				}
  1177			}
  1178			if err := b.addScanCabConfig(tlsO); err != nil {
  1179				return nil, fmt.Errorf("Could not generate config for scanning cabinet: %v", err)
  1180			}
  1181		}
  1182	
  1183		if b.runIndex() {
  1184			b.addUIConfig()
  1185			sto, err := b.sortedStorage("index")
  1186			if err != nil {
  1187				return nil, err
  1188			}
  1189			b.addPrefix("/index/", "storage-index", args{
  1190				"blobSource": "/bs/",
  1191				"storage":    sto,
  1192			})
  1193		}
  1194	
  1195		if conf.S3 != "" {
  1196			if err := b.addS3Config(conf.S3); err != nil {
  1197				return nil, err
  1198			}
  1199		}
  1200		if conf.B2 != "" {
  1201			if err := b.addB2Config(conf.B2); err != nil {
  1202				return nil, err
  1203			}
  1204		}
  1205		if conf.GoogleDrive != "" {
  1206			if err := b.addGoogleDriveConfig(conf.GoogleDrive); err != nil {
  1207				return nil, err
  1208			}
  1209		}
  1210		if conf.GoogleCloudStorage != "" {
  1211			if err := b.addGoogleCloudStorageConfig(conf.GoogleCloudStorage); err != nil {
  1212				return nil, err
  1213			}
  1214		}
  1215	
  1216		return &Config{jconf: b.low}, nil
  1217	}
  1218	
  1219	func numSet(vv ...interface{}) (num int) {
  1220		for _, vi := range vv {
  1221			switch v := vi.(type) {
  1222			case string:
  1223				if v != "" {
  1224					num++
  1225				}
  1226			case bool:
  1227				if v {
  1228					num++
  1229				}
  1230			default:
  1231				panic("unknown type")
  1232			}
  1233		}
  1234		return
  1235	}
  1236	
  1237	var defaultBaseConfig = serverconfig.Config{
  1238		Listen: ":3179",
  1239		HTTPS:  false,
  1240		Auth:   "localhost",
  1241	}
  1242	
  1243	// WriteDefaultConfigFile generates a new default high-level server configuration
  1244	// file at filePath. If useSQLite, the default indexer will use SQLite, otherwise
  1245	// leveldb. If filePath already exists, it is overwritten.
  1246	func WriteDefaultConfigFile(filePath string, useSQLite bool) error {
  1247		conf := defaultBaseConfig
  1248		blobDir, err := osutil.CamliBlobRoot()
  1249		if err != nil {
  1250			return err
  1251		}
  1252		varDir, err := osutil.CamliVarDir()
  1253		if err != nil {
  1254			return err
  1255		}
  1256		if err := wkfs.MkdirAll(blobDir, 0700); err != nil {
  1257			return fmt.Errorf("Could not create default blobs directory: %v", err)
  1258		}
  1259		conf.BlobPath = blobDir
  1260		conf.PackRelated = true
  1261	
  1262		if useSQLite {
  1263			conf.SQLite = filepath.Join(varDir, "index.sqlite")
  1264		} else {
  1265			conf.LevelDB = filepath.Join(varDir, "index.leveldb")
  1266		}
  1267	
  1268		keyID, secretRing, err := getOrMakeKeyring()
  1269		if err != nil {
  1270			return err
  1271		}
  1272		conf.Identity = keyID
  1273		conf.IdentitySecretRing = secretRing
  1274	
  1275		confData, err := json.MarshalIndent(conf, "", "    ")
  1276		if err != nil {
  1277			return fmt.Errorf("Could not json encode config file : %v", err)
  1278		}
  1279	
  1280		if err := wkfs.WriteFile(filePath, confData, 0600); err != nil {
  1281			return fmt.Errorf("Could not create or write default server config: %v", err)
  1282		}
  1283	
  1284		return nil
  1285	}
  1286	
  1287	func getOrMakeKeyring() (keyID, secRing string, err error) {
  1288		secRing = osutil.SecretRingFile()
  1289		_, err = wkfs.Stat(secRing)
  1290		switch {
  1291		case err == nil:
  1292			keyID, err = jsonsign.KeyIdFromRing(secRing)
  1293			if err != nil {
  1294				err = fmt.Errorf("Could not find any keyID in file %q: %v", secRing, err)
  1295				return
  1296			}
  1297			log.Printf("Re-using identity with keyID %q found in file %s", keyID, secRing)
  1298		case os.IsNotExist(err):
  1299			keyID, err = jsonsign.GenerateNewSecRing(secRing)
  1300			if err != nil {
  1301				err = fmt.Errorf("Could not generate new secRing at file %q: %v", secRing, err)
  1302				return
  1303			}
  1304			log.Printf("Generated new identity with keyID %q in file %s", keyID, secRing)
  1305		default:
  1306			err = fmt.Errorf("Could not stat secret ring %q: %v", secRing, err)
  1307		}
  1308		return
  1309	}
Website layout inspired by memcached.
Content by the authors.