Files
dolt/go/spec/dataspec.go
2016-07-06 15:42:21 -07:00

249 lines
5.7 KiB
Go

// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
package spec
import (
"flag"
"fmt"
"net/url"
"regexp"
"strings"
"github.com/attic-labs/noms/go/chunks"
"github.com/attic-labs/noms/go/d"
"github.com/attic-labs/noms/go/datas"
"github.com/attic-labs/noms/go/dataset"
"github.com/attic-labs/noms/go/types"
)
var (
datasetRe = regexp.MustCompile("^" + dataset.DatasetRe.String() + "$")
ldbStores = map[string]*refCountingLdbStore{}
)
func GetDatabase(str string) (datas.Database, error) {
sp, err := parseDatabaseSpec(str)
if err != nil {
return nil, err
}
return sp.Database()
}
func GetChunkStore(str string) (chunks.ChunkStore, error) {
sp, err := parseDatabaseSpec(str)
if err != nil {
return nil, err
}
switch sp.Protocol {
case "ldb":
return getLDBStore(sp.Path), nil
case "mem":
return chunks.NewMemoryStore(), nil
default:
return nil, fmt.Errorf("Unable to create chunkstore for protocol: %s", str)
}
}
func GetDataset(str string) (dataset.Dataset, error) {
sp, err := parseDatasetSpec(str)
if err != nil {
return dataset.Dataset{}, err
}
return sp.Dataset()
}
func GetPath(str string) (datas.Database, types.Value, error) {
sp, err := parsePathSpec(str)
if err != nil {
return nil, nil, err
}
return sp.Value()
}
type databaseSpec struct {
Protocol string
Path string
accessToken string
}
type datasetSpec struct {
DbSpec databaseSpec
DatasetName string
}
type pathSpec struct {
DbSpec databaseSpec
Path AbsolutePath
}
func parseDatabaseSpec(spec string) (databaseSpec, error) {
ldbDatabaseSpec := func(path string) (databaseSpec, error) {
if len(path) == 0 {
return databaseSpec{}, fmt.Errorf("Empty file system path")
}
return databaseSpec{Protocol: "ldb", Path: path}, nil
}
parts := strings.SplitN(spec, ":", 2) // [protocol] [, path]?
protocol := parts[0]
// If there was no ":" then this is either a mem spec, or a filesystem path.
// This is ambiguous if the file system path is "mem" but that just means the path needs to be explicitly "ldb:mem".
if len(parts) == 1 {
if protocol == "mem" {
return databaseSpec{Protocol: "mem"}, nil
}
return ldbDatabaseSpec(protocol)
}
path := parts[1]
switch protocol {
case "http", "https":
u, err := url.Parse(spec)
if err != nil || len(u.Host) == 0 {
return databaseSpec{}, fmt.Errorf("Invalid URL: %s", spec)
}
token := u.Query().Get("access_token")
return databaseSpec{Protocol: protocol, Path: path, accessToken: token}, nil
case "ldb":
return ldbDatabaseSpec(path)
case "mem":
return databaseSpec{}, fmt.Errorf(`In-memory database must be specified as "mem", not "mem:%s"`, path)
default:
return databaseSpec{}, fmt.Errorf("Invalid database protocol: %s", spec)
}
}
func splitAndParseDatabaseSpec(spec string) (databaseSpec, string, error) {
parts := strings.SplitN(spec, "::", 2)
if len(parts) != 2 {
return databaseSpec{}, "", fmt.Errorf("Missing :: separator between database and dataset: %s", spec)
}
dbSpec, err := parseDatabaseSpec(parts[0])
if err != nil {
return databaseSpec{}, "", err
}
return dbSpec, parts[1], nil
}
func parseDatasetSpec(spec string) (datasetSpec, error) {
dbSpec, dsName, err := splitAndParseDatabaseSpec(spec)
if err != nil {
return datasetSpec{}, err
}
if !datasetRe.MatchString(dsName) {
return datasetSpec{}, fmt.Errorf("Invalid dataset, must match %s: %s", dataset.DatasetRe.String(), dsName)
}
return datasetSpec{dbSpec, dsName}, nil
}
func parsePathSpec(spec string) (pathSpec, error) {
dbSpec, pathStr, err := splitAndParseDatabaseSpec(spec)
if err != nil {
return pathSpec{}, err
}
path, err := NewAbsolutePath(pathStr)
if err != nil {
return pathSpec{}, err
}
return pathSpec{dbSpec, path}, nil
}
func (s databaseSpec) String() string {
return s.Protocol + ":" + s.Path
}
func (spec databaseSpec) Database() (ds datas.Database, err error) {
switch spec.Protocol {
case "http", "https":
err = d.Unwrap(d.Try(func() {
ds = datas.NewRemoteDatabase(spec.String(), "Bearer "+spec.accessToken)
}))
case "ldb":
err = d.Unwrap(d.Try(func() {
ds = datas.NewDatabase(getLDBStore(spec.Path))
}))
case "mem":
ds = datas.NewDatabase(chunks.NewMemoryStore())
default:
err = fmt.Errorf("Invalid path prototocol: %s", spec.Protocol)
}
return
}
func (spec datasetSpec) Dataset() (dataset.Dataset, error) {
store, err := spec.DbSpec.Database()
if err != nil {
return dataset.Dataset{}, err
}
return dataset.NewDataset(store, spec.DatasetName), nil
}
func (s datasetSpec) String() string {
return s.DbSpec.String() + "::" + s.DatasetName
}
func (spec datasetSpec) Value() (datas.Database, types.Value, error) {
dataset, err := spec.Dataset()
if err != nil {
return nil, nil, err
}
commit, ok := dataset.MaybeHead()
if !ok {
dataset.Database().Close()
return nil, nil, fmt.Errorf("No head value for dataset: %s", spec.DatasetName)
}
return dataset.Database(), commit, nil
}
func (spec pathSpec) Value() (db datas.Database, val types.Value, err error) {
db, err = spec.DbSpec.Database()
if err != nil {
return
}
val = spec.Path.Resolve(db)
return
}
func RegisterDatabaseFlags(flags *flag.FlagSet) {
chunks.RegisterLevelDBFlags(flags)
}
func CreateDatabaseSpecString(protocol, path string) string {
return fmt.Sprintf("%s:%s", protocol, path)
}
func CreateValueSpecString(protocol, path, value string) string {
return fmt.Sprintf("%s:%s::%s", protocol, path, value)
}
func getLDBStore(path string) chunks.ChunkStore {
if store, ok := ldbStores[path]; ok {
store.AddRef()
return store
}
store := newRefCountingLdbStore(path, func() {
delete(ldbStores, path)
})
ldbStores[path] = store
return store
}