github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/dropped_databases.go (about) 1 // Copyright 2023 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 sqle 16 17 import ( 18 "fmt" 19 "path/filepath" 20 "strings" 21 "time" 22 23 "github.com/dolthub/go-mysql-server/sql" 24 25 "github.com/dolthub/dolt/go/libraries/doltcore/dbfactory" 26 "github.com/dolthub/dolt/go/libraries/utils/errors" 27 "github.com/dolthub/dolt/go/libraries/utils/filesys" 28 ) 29 30 // droppedDatabaseDirectoryName is the subdirectory within the data folder where Dolt moves databases after they are 31 // dropped. The dolt_undrop() stored procedure is then able to restore them from this location. 32 const droppedDatabaseDirectoryName = ".dolt_dropped_databases" 33 34 // droppedDatabaseManager is responsible for dropping databases and "undropping", or restoring, dropped databases. It 35 // is given a Filesys where all database directories can be found. When dropping a database, instead of deleting the 36 // database directory, it will move it to a new ".dolt_dropped_databases" directory where databases can be restored. 37 type droppedDatabaseManager struct { 38 fs filesys.Filesys 39 } 40 41 // newDroppedDatabaseManager creates a new droppedDatabaseManager instance using the specified |fs| as the location 42 // where databases can be found. It will create a new ".dolt_dropped_databases" directory at the root of |fs| where 43 // dropped databases will be moved until they are permanently removed. 44 func newDroppedDatabaseManager(fs filesys.Filesys) *droppedDatabaseManager { 45 return &droppedDatabaseManager{ 46 fs: fs, 47 } 48 } 49 50 // DropDatabase will move the database directory for the database named |name| at the location |dropDbLoc| to the 51 // dolt_dropped_database directory where it can later be "undropped" to restore it. If any problems are encountered 52 // moving the database directory, an error is returned. 53 func (dd *droppedDatabaseManager) DropDatabase(ctx *sql.Context, name string, dropDbLoc string) error { 54 rootDbLoc, err := dd.fs.Abs("") 55 if err != nil { 56 return err 57 } 58 59 isRootDatabase := false 60 // if the database is in the directory itself, we remove '.dolt' directory rather than 61 // the whole directory itself because it can have other databases that are nested. 62 if rootDbLoc == dropDbLoc { 63 doltDirExists, _ := dd.fs.Exists(dbfactory.DoltDir) 64 if !doltDirExists { 65 return sql.ErrDatabaseNotFound.New(name) 66 } 67 dropDbLoc = filepath.Join(dropDbLoc, dbfactory.DoltDir) 68 isRootDatabase = true 69 } 70 71 if err = dd.initializeDeletedDatabaseDirectory(); err != nil { 72 return fmt.Errorf("unable to drop database %s: %w", name, err) 73 } 74 75 // Move the dropped database to the Dolt deleted database directory so it can be restored if needed 76 _, file := filepath.Split(dropDbLoc) 77 var destinationDirectory string 78 if isRootDatabase { 79 // For a root database, first create the subdirectory before we copy over the .dolt directory 80 newSubdirectory := filepath.Join(droppedDatabaseDirectoryName, name) 81 if err := dd.fs.MkDirs(newSubdirectory); err != nil { 82 return err 83 } 84 destinationDirectory = filepath.Join(newSubdirectory, file) 85 } else { 86 destinationDirectory = filepath.Join(droppedDatabaseDirectoryName, file) 87 } 88 89 // Add the final directory segment and convert any invalid chars so that the physical directory 90 // name matches the current logical/SQL name of the database. 91 dir, base := filepath.Split(destinationDirectory) 92 base = dbfactory.DirToDBName(file) 93 destinationDirectory = filepath.Join(dir, base) 94 95 if err := dd.prepareToMoveDroppedDatabase(ctx, destinationDirectory); err != nil { 96 return err 97 } 98 99 return dd.fs.MoveDir(dropDbLoc, destinationDirectory) 100 } 101 102 // UndropDatabase will restore the database named |name| by moving it from the dolt_dropped_database directory, back 103 // into the root of the filesystem where database directories are managed. This function returns the new location of 104 // the database directory and the exact name (case-sensitive) of the database. If any errors are encountered while 105 // attempting to undrop the database, an error is returned and other return parameters should be ignored. 106 func (dd *droppedDatabaseManager) UndropDatabase(ctx *sql.Context, name string) (filesys.Filesys, string, error) { 107 sourcePath, destinationPath, exactCaseName, err := dd.validateUndropDatabase(ctx, name) 108 if err != nil { 109 return nil, "", err 110 } 111 112 err = dd.fs.MoveDir(sourcePath, destinationPath) 113 if err != nil { 114 return nil, "", err 115 } 116 117 newFs, err := dd.fs.WithWorkingDir(exactCaseName) 118 if err != nil { 119 return nil, "", err 120 } 121 122 return newFs, exactCaseName, nil 123 } 124 125 // PurgeAllDroppedDatabases permanently removes all dropped databases that are being held in the dolt_dropped_database 126 // holding directory. Once dropped databases are purged, they can no longer be restored, so this method should be used 127 // with caution. 128 func (dd *droppedDatabaseManager) PurgeAllDroppedDatabases(_ *sql.Context) error { 129 // If the dropped database holding directory doesn't exist, then there's nothing to purge 130 if exists, _ := dd.fs.Exists(droppedDatabaseDirectoryName); !exists { 131 return nil 132 } 133 134 var err error 135 callback := func(path string, size int64, isDir bool) (stop bool) { 136 // Sanity check that the path we're about to delete is under the dropped database holding directory 137 if strings.Contains(path, droppedDatabaseDirectoryName) == false { 138 err = fmt.Errorf("path of database to purge isn't under dropped database holding directory: %s", path) 139 return true 140 } 141 142 // Attempt to permanently delete the dropped database and stop execution if we hit an error 143 err = dd.fs.Delete(path, true) 144 return err != nil 145 } 146 iterErr := dd.fs.Iter(droppedDatabaseDirectoryName, false, callback) 147 if iterErr != nil { 148 return iterErr 149 } 150 151 return err 152 } 153 154 // initializeDeletedDatabaseDirectory initializes the special directory Dolt uses to store dropped databases until 155 // they are fully removed. If the directory is already created and set up correctly, then this method is a no-op. 156 // If the directory doesn't exist yet, it will be created. If there are any problems initializing the directory, an 157 // error is returned. 158 func (dd *droppedDatabaseManager) initializeDeletedDatabaseDirectory() error { 159 exists, isDir := dd.fs.Exists(droppedDatabaseDirectoryName) 160 if exists && !isDir { 161 return fmt.Errorf("%s exists, but is not a directory", droppedDatabaseDirectoryName) 162 } 163 164 if exists { 165 return nil 166 } 167 168 return dd.fs.MkDirs(droppedDatabaseDirectoryName) 169 } 170 171 func (dd *droppedDatabaseManager) ListDroppedDatabases(_ *sql.Context) ([]string, error) { 172 if err := dd.initializeDeletedDatabaseDirectory(); err != nil { 173 return nil, fmt.Errorf("unable to list undroppable database: %w", err) 174 } 175 176 databaseNames := make([]string, 0, 5) 177 callback := func(path string, size int64, isDir bool) (stop bool) { 178 // When we move a database to the dropped database directory, we normalize the physical directory 179 // name to be the same as the logical SQL name, so there's no need to do any name mapping here. 180 databaseNames = append(databaseNames, filepath.Base(path)) 181 return false 182 } 183 184 if err := dd.fs.Iter(droppedDatabaseDirectoryName, false, callback); err != nil { 185 return nil, err 186 } 187 188 return databaseNames, nil 189 } 190 191 // validateUndropDatabase validates that the database |name| is available to be "undropped" and that no existing 192 // database is already being managed that has the same (case-insensitive) name. If any problems are encountered, 193 // an error is returned. 194 func (dd *droppedDatabaseManager) validateUndropDatabase(ctx *sql.Context, name string) (sourcePath, destinationPath, exactCaseName string, err error) { 195 availableDatabases, err := dd.ListDroppedDatabases(ctx) 196 if err != nil { 197 return "", "", "", err 198 } 199 200 found, exactCaseName := hasCaseInsensitiveMatch(availableDatabases, name) 201 if !found { 202 return "", "", "", fmt.Errorf("no database named '%s' found to undrop. %s", 203 name, errors.CreateUndropErrorMessage(availableDatabases)) 204 } 205 206 // Check to see if the destination directory for restoring the database already exists (case-insensitive match) 207 destinationPath, err = dd.fs.Abs(exactCaseName) 208 if err != nil { 209 return "", "", "", err 210 } 211 212 if hasCaseInsensitivePath(dd.fs, destinationPath) { 213 return "", "", "", fmt.Errorf("unable to undrop database '%s'; "+ 214 "another database already exists with the same case-insensitive name", exactCaseName) 215 } 216 217 sourcePath = filepath.Join(droppedDatabaseDirectoryName, exactCaseName) 218 return sourcePath, destinationPath, exactCaseName, nil 219 } 220 221 // hasCaseInsensitivePath returns true if the specified path |target| already exists on the filesystem |fs|, with 222 // a case-insensitive match on the final component of the path. Note that only the final component of the path is 223 // checked in a case-insensitive match – the other components of the path must be a case-sensitive match. 224 func hasCaseInsensitivePath(fs filesys.Filesys, target string) bool { 225 found := false 226 fs.Iter(filepath.Dir(target), false, func(path string, size int64, isDir bool) (stop bool) { 227 if strings.ToLower(filepath.Base(path)) == strings.ToLower(filepath.Base(target)) { 228 found = true 229 } 230 return found 231 }) 232 return found 233 } 234 235 // hasCaseInsensitiveMatch tests to see if any of |candidates| are a case-insensitive match for |target| and if so, 236 // returns true along with the exact candidate string that matched. If there was not a match, false and the empty 237 // string are returned. 238 func hasCaseInsensitiveMatch(candidates []string, target string) (bool, string) { 239 found := false 240 exactCaseName := "" 241 lowercaseName := strings.ToLower(target) 242 for _, s := range candidates { 243 if lowercaseName == strings.ToLower(s) { 244 exactCaseName = s 245 found = true 246 break 247 } 248 } 249 250 return found, exactCaseName 251 } 252 253 // prepareToMoveDroppedDatabase checks the specified |targetPath| to make sure there is not already a dropped database 254 // there, and if so, the existing dropped database will be renamed with a unique suffix. If any problems are encountered, 255 // such as not being able to rename an existing dropped database, this function will return an error. 256 func (dd *droppedDatabaseManager) prepareToMoveDroppedDatabase(_ *sql.Context, targetPath string) error { 257 if exists, _ := dd.fs.Exists(targetPath); !exists { 258 // If there's nothing at the desired targetPath, we're all set 259 return nil 260 } 261 262 // If there is something already there, pick a new path to move it to 263 newPath := fmt.Sprintf("%s.backup.%d", targetPath, time.Now().UnixMilli()) 264 if exists, _ := dd.fs.Exists(newPath); exists { 265 return fmt.Errorf("unable to move existing dropped database out of the way: "+ 266 "tried to move it to %s", newPath) 267 } 268 if err := dd.fs.MoveDir(targetPath, newPath); err != nil { 269 return fmt.Errorf("unable to move existing dropped database out of the way: %w", err) 270 } 271 272 return nil 273 }