github.com/hasnat/dolt/go@v0.0.0-20210628190320-9eb5d843fbb7/libraries/doltcore/env/multi_repo_env.go (about)

     1  // Copyright 2020 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package env
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"net/url"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  	"unicode"
    25  
    26  	"github.com/dolthub/dolt/go/libraries/utils/earl"
    27  
    28  	"github.com/dolthub/dolt/go/libraries/doltcore/dbfactory"
    29  	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
    30  	"github.com/dolthub/dolt/go/libraries/utils/filesys"
    31  )
    32  
    33  // EnvNameAndPath is a simple tuple of the name of an environment and the path to where it is on disk
    34  type EnvNameAndPath struct {
    35  	// Name is the name of the environment and is used as the identifier when accessing a given environment
    36  	Name string
    37  	// Path is the path on disk to where the environment lives
    38  	Path string
    39  }
    40  
    41  // MultiRepoEnv is a type used to store multiple environments which can be retrieved by name
    42  type MultiRepoEnv map[string]*DoltEnv
    43  
    44  // AddEnv adds an environment to the MultiRepoEnv by name
    45  func (mrEnv MultiRepoEnv) AddEnv(name string, dEnv *DoltEnv) {
    46  	mrEnv[name] = dEnv
    47  }
    48  
    49  // Iter iterates over all environments in the MultiRepoEnv
    50  func (mrEnv MultiRepoEnv) Iter(cb func(name string, dEnv *DoltEnv) (stop bool, err error)) error {
    51  	for name, dEnv := range mrEnv {
    52  		stop, err := cb(name, dEnv)
    53  
    54  		if err != nil {
    55  			return err
    56  		}
    57  
    58  		if stop {
    59  			break
    60  		}
    61  	}
    62  
    63  	return nil
    64  }
    65  
    66  // GetWorkingRoots returns a map with entries for each environment name with a value equal to the working root
    67  // for that environment
    68  func (mrEnv MultiRepoEnv) GetWorkingRoots(ctx context.Context) (map[string]*doltdb.RootValue, error) {
    69  	roots := make(map[string]*doltdb.RootValue)
    70  	err := mrEnv.Iter(func(name string, dEnv *DoltEnv) (stop bool, err error) {
    71  		root, err := dEnv.WorkingRoot(ctx)
    72  
    73  		if err != nil {
    74  			return true, err
    75  		}
    76  
    77  		roots[name] = root
    78  		return false, nil
    79  	})
    80  
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	return roots, err
    86  }
    87  
    88  func getRepoRootDir(path, pathSeparator string) string {
    89  	if pathSeparator != "/" {
    90  		path = strings.ReplaceAll(path, pathSeparator, "/")
    91  	}
    92  
    93  	// filepath.Clean does not work with cross platform paths.  So can't test a windows path on a mac
    94  	tokens := strings.Split(path, "/")
    95  
    96  	for i := len(tokens) - 1; i >= 0; i-- {
    97  		if tokens[i] == "" {
    98  			tokens = append(tokens[:i], tokens[i+1:]...)
    99  		}
   100  	}
   101  
   102  	if len(tokens) == 0 {
   103  		return ""
   104  	}
   105  
   106  	if tokens[len(tokens)-1] == dbfactory.DataDir && tokens[len(tokens)-2] == dbfactory.DoltDir {
   107  		tokens = tokens[:len(tokens)-2]
   108  	}
   109  
   110  	if len(tokens) == 0 {
   111  		return ""
   112  	}
   113  
   114  	name := tokens[len(tokens)-1]
   115  
   116  	// handles drive letters. fine with a folder containing a colon having the default name
   117  	if strings.IndexRune(name, ':') != -1 {
   118  		return ""
   119  	}
   120  
   121  	return name
   122  }
   123  
   124  // DoltEnvAsMultiEnv returns a MultiRepoEnv which wraps the DoltEnv and names it based on the directory DoltEnv refers to
   125  func DoltEnvAsMultiEnv(dEnv *DoltEnv) (MultiRepoEnv, error) {
   126  	dbName := "dolt"
   127  
   128  	if dEnv.RSLoadErr != nil {
   129  		return nil, fmt.Errorf("error loading environment: %s", dEnv.RSLoadErr.Error())
   130  	} else if dEnv.DBLoadError != nil {
   131  		return nil, fmt.Errorf("error loading environment: %s", dEnv.DBLoadError.Error())
   132  	} else if dEnv.CfgLoadErr != nil {
   133  		return nil, fmt.Errorf("error loading environment: %s", dEnv.CfgLoadErr.Error())
   134  	}
   135  
   136  	u, err := earl.Parse(dEnv.urlStr)
   137  
   138  	if err == nil {
   139  		if u.Scheme == dbfactory.FileScheme {
   140  			path, err := url.PathUnescape(u.Path)
   141  
   142  			if err == nil {
   143  				path, err = dEnv.FS.Abs(path)
   144  
   145  				if err == nil {
   146  					dirName := getRepoRootDir(path, string(os.PathSeparator))
   147  
   148  					if dirName != "" {
   149  						dbName = dirToDBName(dirName)
   150  					}
   151  				}
   152  			}
   153  		}
   154  	}
   155  
   156  	mrEnv := make(MultiRepoEnv)
   157  	mrEnv.AddEnv(dbName, dEnv)
   158  
   159  	return mrEnv, nil
   160  }
   161  
   162  // LoadMultiEnv takes a variable list of EnvNameAndPath objects loads each of the environments, and returns a new
   163  // MultiRepoEnv
   164  func LoadMultiEnv(ctx context.Context, hdp HomeDirProvider, fs filesys.Filesys, version string, envNamesAndPaths ...EnvNameAndPath) (MultiRepoEnv, error) {
   165  	nameToPath := make(map[string]string)
   166  	for _, nameAndPath := range envNamesAndPaths {
   167  		existingPath, ok := nameToPath[nameAndPath.Name]
   168  
   169  		if ok {
   170  			if existingPath == nameAndPath.Path {
   171  				continue
   172  			}
   173  
   174  			return nil, fmt.Errorf("databases at paths '%s' and '%s' both attempted to load with the name '%s'", existingPath, nameAndPath.Path, nameAndPath.Name)
   175  		}
   176  
   177  		nameToPath[nameAndPath.Name] = nameAndPath.Path
   178  	}
   179  
   180  	mrEnv := make(MultiRepoEnv)
   181  	for name, path := range nameToPath {
   182  		absPath, err := fs.Abs(path)
   183  
   184  		if err != nil {
   185  			return nil, err
   186  		}
   187  
   188  		fsForEnv, err := filesys.LocalFilesysWithWorkingDir(absPath)
   189  
   190  		if err != nil {
   191  			return nil, err
   192  		}
   193  
   194  		urlStr := earl.FileUrlFromPath(filepath.Join(absPath, dbfactory.DoltDataDir), os.PathSeparator)
   195  		dEnv := Load(ctx, hdp, fsForEnv, urlStr, version)
   196  
   197  		if dEnv.RSLoadErr != nil {
   198  			return nil, fmt.Errorf("error loading environment '%s' at path '%s': %s", name, absPath, dEnv.RSLoadErr.Error())
   199  		} else if dEnv.DBLoadError != nil {
   200  			return nil, fmt.Errorf("error loading environment '%s' at path '%s': %s", name, absPath, dEnv.DBLoadError.Error())
   201  		} else if dEnv.CfgLoadErr != nil {
   202  			return nil, fmt.Errorf("error loading environment '%s' at path '%s': %s", name, absPath, dEnv.CfgLoadErr.Error())
   203  		}
   204  
   205  		mrEnv.AddEnv(name, dEnv)
   206  	}
   207  
   208  	return mrEnv, nil
   209  }
   210  
   211  func DBNamesAndPathsFromDir(fs filesys.Filesys, path string) ([]EnvNameAndPath, error) {
   212  	var envNamesAndPaths []EnvNameAndPath
   213  	err := fs.Iter(path, false, func(path string, size int64, isDir bool) (stop bool) {
   214  		if isDir {
   215  			dirName := filepath.Base(path)
   216  			if dirName[0] == '.' {
   217  				return false
   218  			}
   219  
   220  			name := dirToDBName(dirName)
   221  			envNamesAndPaths = append(envNamesAndPaths, EnvNameAndPath{Name: name, Path: path})
   222  		}
   223  
   224  		return false
   225  	})
   226  
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  
   231  	return envNamesAndPaths, nil
   232  }
   233  
   234  // LoadMultiEnvFromDir looks at each subfolder of the given path as a Dolt repository and attempts to return a MultiRepoEnv
   235  // with initialized environments for each of those subfolder data repositories. subfolders whose name starts with '.' are
   236  // skipped.
   237  func LoadMultiEnvFromDir(ctx context.Context, hdp HomeDirProvider, fs filesys.Filesys, path, version string) (MultiRepoEnv, error) {
   238  	envNamesAndPaths, err := DBNamesAndPathsFromDir(fs, path)
   239  
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  
   244  	return LoadMultiEnv(ctx, hdp, fs, version, envNamesAndPaths...)
   245  }
   246  
   247  func dirToDBName(dirName string) string {
   248  	dbName := strings.TrimSpace(dirName)
   249  	dbName = strings.Map(func(r rune) rune {
   250  		if unicode.IsSpace(r) || r == '-' {
   251  			return '_'
   252  		}
   253  		return r
   254  	}, dbName)
   255  
   256  	newDBName := strings.ReplaceAll(dbName, "__", "_")
   257  
   258  	for dbName != newDBName {
   259  		dbName = newDBName
   260  		newDBName = strings.ReplaceAll(dbName, "__", "_")
   261  	}
   262  
   263  	return dbName
   264  }