github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/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 "errors" 20 "os" 21 "path/filepath" 22 "sort" 23 "strings" 24 25 "github.com/sirupsen/logrus" 26 27 "github.com/dolthub/dolt/go/libraries/doltcore/dbfactory" 28 "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" 29 "github.com/dolthub/dolt/go/libraries/utils/config" 30 "github.com/dolthub/dolt/go/libraries/utils/filesys" 31 "github.com/dolthub/dolt/go/libraries/utils/set" 32 "github.com/dolthub/dolt/go/store/types" 33 ) 34 35 // EnvNameAndPath is a simple tuple of the name of an environment and the path to where it is on disk 36 type EnvNameAndPath struct { 37 // Name is the name of the environment and is used as the identifier when accessing a given environment 38 Name string 39 // Path is the path on disk to where the environment lives 40 Path string 41 } 42 43 type NamedEnv struct { 44 name string 45 env *DoltEnv 46 } 47 48 // MultiRepoEnv is a type used to store multiple environments which can be retrieved by name 49 type MultiRepoEnv struct { 50 envs []NamedEnv 51 fs filesys.Filesys 52 cfg config.ReadWriteConfig 53 dialProvider dbfactory.GRPCDialProvider 54 } 55 56 // NewMultiEnv returns a new MultiRepoEnv instance dirived from a root DoltEnv instance. 57 func MultiEnvForSingleEnv(ctx context.Context, env *DoltEnv) (*MultiRepoEnv, error) { 58 return MultiEnvForDirectory(ctx, env.Config.WriteableConfig(), env.FS, env.Version, env) 59 } 60 61 // MultiEnvForDirectory returns a MultiRepoEnv for the directory rooted at the file system given. The doltEnv from the 62 // invoking context is included. If it's non-nil and valid, it will be included in the returned MultiRepoEnv, and will 63 // be the first database in all iterations. 64 func MultiEnvForDirectory( 65 ctx context.Context, 66 config config.ReadWriteConfig, 67 dataDirFS filesys.Filesys, 68 version string, 69 dEnv *DoltEnv, 70 ) (*MultiRepoEnv, error) { 71 // Load current dataDirFS and put into mr env 72 var dbName string = "dolt" 73 var newDEnv *DoltEnv = dEnv 74 75 // InMemFS is used only for testing. 76 // All other FS Types should get a newly created Environment which will serve as the primary env in the MultiRepoEnv 77 if _, ok := dataDirFS.(*filesys.InMemFS); !ok { 78 path, err := dataDirFS.Abs("") 79 if err != nil { 80 return nil, err 81 } 82 envName := getRepoRootDir(path, string(os.PathSeparator)) 83 dbName = dbfactory.DirToDBName(envName) 84 85 newDEnv = Load(ctx, GetCurrentUserHomeDir, dataDirFS, doltdb.LocalDirDoltDB, version) 86 } 87 88 mrEnv := &MultiRepoEnv{ 89 envs: make([]NamedEnv, 0), 90 fs: dataDirFS, 91 cfg: config, 92 dialProvider: NewGRPCDialProviderFromDoltEnv(newDEnv), 93 } 94 95 envSet := map[string]*DoltEnv{} 96 if newDEnv.Valid() { 97 envSet[dbName] = newDEnv 98 } 99 100 // If there are other directories in the directory, try to load them as additional databases 101 dataDirFS.Iter(".", false, func(path string, size int64, isDir bool) (stop bool) { 102 if !isDir { 103 return false 104 } 105 106 dir := filepath.Base(path) 107 108 newFs, err := dataDirFS.WithWorkingDir(dir) 109 if err != nil { 110 return false 111 } 112 113 // TODO: get rid of version altogether 114 version := "" 115 if dEnv != nil { 116 version = dEnv.Version 117 } 118 119 newEnv := Load(ctx, GetCurrentUserHomeDir, newFs, doltdb.LocalDirDoltDB, version) 120 if newEnv.Valid() { 121 envSet[dbfactory.DirToDBName(dir)] = newEnv 122 } else { 123 dbErr := newEnv.DBLoadError 124 if dbErr != nil { 125 if !errors.Is(dbErr, doltdb.ErrMissingDoltDataDir) { 126 logrus.Warnf("failed to load database at %s with error: %s", path, dbErr.Error()) 127 } 128 } 129 cfgErr := newEnv.CfgLoadErr 130 if cfgErr != nil { 131 logrus.Warnf("failed to load database configuration at %s with error: %s", path, cfgErr.Error()) 132 } 133 } 134 return false 135 }) 136 137 enforceSingleFormat(envSet) 138 139 // if the current directory database is in our set, add it first so it will be the current database 140 if env, ok := envSet[dbName]; ok && env.Valid() { 141 mrEnv.addEnv(dbName, env) 142 delete(envSet, dbName) 143 } 144 145 // get the keys from the envSet keys as a sorted list 146 sortedKeys := make([]string, 0, len(envSet)) 147 for k := range envSet { 148 sortedKeys = append(sortedKeys, k) 149 } 150 sort.Strings(sortedKeys) 151 for _, dbName := range sortedKeys { 152 mrEnv.addEnv(dbName, envSet[dbName]) 153 } 154 155 return mrEnv, nil 156 } 157 158 func (mrEnv *MultiRepoEnv) FileSystem() filesys.Filesys { 159 return mrEnv.fs 160 } 161 162 func (mrEnv *MultiRepoEnv) RemoteDialProvider() dbfactory.GRPCDialProvider { 163 return mrEnv.dialProvider 164 } 165 166 func (mrEnv *MultiRepoEnv) Config() config.ReadWriteConfig { 167 return mrEnv.cfg 168 } 169 170 // addEnv adds an environment to the MultiRepoEnv by name 171 func (mrEnv *MultiRepoEnv) addEnv(name string, dEnv *DoltEnv) { 172 mrEnv.envs = append(mrEnv.envs, NamedEnv{ 173 name: name, 174 env: dEnv, 175 }) 176 } 177 178 // GetEnv returns the env with the name given, or nil if no such env exists 179 func (mrEnv *MultiRepoEnv) GetEnv(name string) *DoltEnv { 180 var found *DoltEnv 181 mrEnv.Iter(func(n string, dEnv *DoltEnv) (stop bool, err error) { 182 if n == name { 183 found = dEnv 184 return true, nil 185 } 186 return false, nil 187 }) 188 return found 189 } 190 191 // Iter iterates over all environments in the MultiRepoEnv 192 func (mrEnv *MultiRepoEnv) Iter(cb func(name string, dEnv *DoltEnv) (stop bool, err error)) error { 193 for _, e := range mrEnv.envs { 194 stop, err := cb(e.name, e.env) 195 196 if err != nil { 197 return err 198 } 199 200 if stop { 201 break 202 } 203 } 204 205 return nil 206 } 207 208 // GetFirstDatabase returns the name of the first database in the MultiRepoEnv. This will be the database in the 209 // current working directory if applicable, or the first database alphabetically otherwise. 210 func (mrEnv *MultiRepoEnv) GetFirstDatabase() string { 211 var currentDb string 212 _ = mrEnv.Iter(func(name string, _ *DoltEnv) (stop bool, err error) { 213 currentDb = name 214 return true, nil 215 }) 216 217 return currentDb 218 } 219 220 func getRepoRootDir(path, pathSeparator string) string { 221 if pathSeparator != "/" { 222 path = strings.ReplaceAll(path, pathSeparator, "/") 223 } 224 225 // filepath.Clean does not work with cross platform paths. So can't test a windows path on a mac 226 tokens := strings.Split(path, "/") 227 228 for i := len(tokens) - 1; i >= 0; i-- { 229 if tokens[i] == "" { 230 tokens = append(tokens[:i], tokens[i+1:]...) 231 } 232 } 233 234 if len(tokens) == 0 { 235 return "" 236 } 237 238 if tokens[len(tokens)-1] == dbfactory.DataDir && tokens[len(tokens)-2] == dbfactory.DoltDir { 239 tokens = tokens[:len(tokens)-2] 240 } 241 242 if len(tokens) == 0 { 243 return "" 244 } 245 246 name := tokens[len(tokens)-1] 247 248 // handles drive letters. fine with a folder containing a colon having the default name 249 if strings.IndexRune(name, ':') != -1 { 250 return "" 251 } 252 253 return name 254 } 255 256 // enforceSingleFormat enforces that constraint that all databases in 257 // a multi-database environment have the same NomsBinFormat. 258 // Databases are removed from the MultiRepoEnv to ensure this is true. 259 func enforceSingleFormat(envSet map[string]*DoltEnv) { 260 formats := set.NewEmptyStrSet() 261 for _, dEnv := range envSet { 262 formats.Add(dEnv.DoltDB.Format().VersionString()) 263 } 264 265 var nbf string 266 // if present, prefer types.Format_Default 267 if ok := formats.Contains(types.Format_Default.VersionString()); ok { 268 nbf = types.Format_Default.VersionString() 269 } else { 270 // otherwise, pick an arbitrary format 271 for _, dEnv := range envSet { 272 nbf = dEnv.DoltDB.Format().VersionString() 273 } 274 } 275 276 template := "incompatible format for database '%s'; expected '%s', found '%s'" 277 for name, dEnv := range envSet { 278 found := dEnv.DoltDB.Format().VersionString() 279 if found != nbf { 280 logrus.Infof(template, name, nbf, found) 281 delete(envSet, name) 282 } 283 } 284 }