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 }