github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/dprocedures/dolt_backup.go (about) 1 // Copyright 2022 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 dprocedures 16 17 import ( 18 "fmt" 19 "strings" 20 21 "github.com/dolthub/go-mysql-server/sql" 22 23 "github.com/dolthub/dolt/go/cmd/dolt/cli" 24 "github.com/dolthub/dolt/go/libraries/doltcore/branch_control" 25 "github.com/dolthub/dolt/go/libraries/doltcore/dbfactory" 26 "github.com/dolthub/dolt/go/libraries/doltcore/env" 27 "github.com/dolthub/dolt/go/libraries/doltcore/env/actions" 28 "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" 29 "github.com/dolthub/dolt/go/libraries/utils/argparser" 30 "github.com/dolthub/dolt/go/libraries/utils/filesys" 31 "github.com/dolthub/dolt/go/store/datas/pull" 32 ) 33 34 const ( 35 DoltBackupFuncName = "dolt_backup" 36 37 statusOk = 0 38 statusErr = 1 39 ) 40 41 // doltBackup is the stored procedure version for the CLI command `dolt backup`. 42 func doltBackup(ctx *sql.Context, args ...string) (sql.RowIter, error) { 43 res, err := doDoltBackup(ctx, args) 44 if err != nil { 45 return nil, err 46 } 47 return rowToIter(int64(res)), nil 48 } 49 50 func doDoltBackup(ctx *sql.Context, args []string) (int, error) { 51 dbName := ctx.GetCurrentDatabase() 52 if len(dbName) == 0 { 53 return statusErr, fmt.Errorf("Empty database name.") 54 } 55 if err := branch_control.CheckAccess(ctx, branch_control.Permissions_Write); err != nil { 56 return statusErr, err 57 } 58 59 apr, err := cli.CreateBackupArgParser().Parse(args) 60 if err != nil { 61 return statusErr, err 62 } 63 64 invalidParams := []string{dbfactory.AWSCredsFileParam, dbfactory.AWSCredsProfile, dbfactory.AWSCredsTypeParam, dbfactory.AWSRegionParam} 65 for _, param := range invalidParams { 66 if apr.Contains(param) { 67 return statusErr, fmt.Errorf("parameter '%s' is not supported when running this command via SQL", param) 68 } 69 } 70 71 sess := dsess.DSessFromSess(ctx.Session) 72 dbData, ok := sess.GetDbData(ctx, dbName) 73 if !ok { 74 return statusErr, sql.ErrDatabaseNotFound.New(dbName) 75 } 76 77 if apr.NArg() == 0 { 78 return statusErr, fmt.Errorf("listing existing backup endpoints in sql is not currently implemented. Let us know if you need this by opening a GitHub issue: https://github.com/dolthub/dolt/issues") 79 80 } 81 switch apr.Arg(0) { 82 case cli.AddBackupId: 83 err = addBackup(ctx, dbData, apr) 84 if err != nil { 85 return statusErr, fmt.Errorf("error adding backup: %w", err) 86 } 87 case cli.RemoveBackupId, cli.RemoveBackupShortId: 88 err = removeBackup(ctx, dbData, apr) 89 if err != nil { 90 return statusErr, fmt.Errorf("error removing backup: %w", err) 91 } 92 case cli.RestoreBackupId: 93 return statusErr, fmt.Errorf("restoring backup endpoint in sql is unimplemented.") 94 case cli.SyncBackupUrlId: 95 err = syncBackupViaUrl(ctx, dbData, sess, apr) 96 if err != nil { 97 return statusErr, fmt.Errorf("error syncing backup url: %w", err) 98 } 99 case cli.SyncBackupId: 100 err = syncBackupViaName(ctx, dbData, sess, apr) 101 if err != nil { 102 return statusErr, fmt.Errorf("error syncing backup: %w", err) 103 } 104 default: 105 return statusErr, fmt.Errorf("unrecognized dolt_backup parameter: %s", apr.Arg(0)) 106 } 107 108 return statusOk, nil 109 } 110 111 func addBackup(ctx *sql.Context, dbData env.DbData, apr *argparser.ArgParseResults) error { 112 if apr.NArg() != 3 { 113 return fmt.Errorf("usage: dolt_backup('add', 'backup_name', 'backup-url')") 114 } 115 116 backupName := strings.TrimSpace(apr.Arg(1)) 117 backupUrl := apr.Arg(2) 118 cfg := loadConfig(ctx) 119 scheme, absBackupUrl, err := env.GetAbsRemoteUrl(filesys.LocalFS, cfg, backupUrl) 120 if err != nil { 121 return fmt.Errorf("error: '%s' is not valid, %s", backupUrl, err.Error()) 122 } else if scheme == dbfactory.HTTPScheme || scheme == dbfactory.HTTPSScheme { 123 // not sure how to get the dialer so punting on this 124 return fmt.Errorf("sync-url does not support http or https backup locations currently") 125 } 126 127 params, err := cli.ProcessBackupArgs(apr, scheme, absBackupUrl) 128 if err != nil { 129 return err 130 } 131 132 r := env.NewRemote(backupName, absBackupUrl, params) 133 err = dbData.Rsw.AddBackup(r) 134 switch err { 135 case nil: 136 return nil 137 case env.ErrBackupAlreadyExists: 138 return fmt.Errorf("error: a backup named '%s' already exists, remove it before running this command again", r.Name) 139 case env.ErrBackupNotFound: 140 return fmt.Errorf("error: unknown backup: '%s' ", r.Name) 141 case env.ErrInvalidBackupURL: 142 return fmt.Errorf("error: '%s' is not valid, cause: %s", r.Url, err.Error()) 143 case env.ErrInvalidBackupName: 144 return fmt.Errorf("error: invalid backup name: '%s'", r.Name) 145 default: 146 return fmt.Errorf("error: Unable to save changes, cause: %s", err.Error()) 147 } 148 } 149 150 func removeBackup(ctx *sql.Context, dbData env.DbData, apr *argparser.ArgParseResults) error { 151 if apr.NArg() != 2 { 152 return fmt.Errorf("usage: dolt_backup('remove', 'backup_name'") 153 } 154 155 backupName := strings.TrimSpace(apr.Arg(1)) 156 err := dbData.Rsw.RemoveBackup(ctx, backupName) 157 switch err { 158 case nil: 159 return nil 160 case env.ErrFailedToWriteRepoState: 161 return fmt.Errorf("error: failed to save change to repo state, cause: %s", err.Error()) 162 case env.ErrFailedToDeleteBackup: 163 return fmt.Errorf("error: failed to delete backup tracking ref, cause: %s", err.Error()) 164 case env.ErrFailedToReadFromDb: 165 return fmt.Errorf("error: failed to read from db, cause: %s", err.Error()) 166 case env.ErrBackupNotFound: 167 return fmt.Errorf("error: unknown backup: '%s' ", backupName) 168 default: 169 return fmt.Errorf("error: unknown error, cause: %s", err.Error()) 170 } 171 } 172 173 func syncBackupViaUrl(ctx *sql.Context, dbData env.DbData, sess *dsess.DoltSession, apr *argparser.ArgParseResults) error { 174 if apr.NArg() != 2 { 175 return fmt.Errorf("usage: dolt_backup('sync-url', BACKUP_URL)") 176 } 177 178 backupUrl := strings.TrimSpace(apr.Arg(1)) 179 cfg := loadConfig(ctx) 180 scheme, absBackupUrl, err := env.GetAbsRemoteUrl(filesys.LocalFS, cfg, backupUrl) 181 if err != nil { 182 return fmt.Errorf("error: '%s' is not valid.", backupUrl) 183 } else if scheme == dbfactory.HTTPScheme || scheme == dbfactory.HTTPSScheme { 184 // not sure how to get the dialer so punting on this 185 return fmt.Errorf("sync-url does not support http or https backup locations currently") 186 } 187 188 params, err := cli.ProcessBackupArgs(apr, scheme, absBackupUrl) 189 if err != nil { 190 return err 191 } 192 193 credsFile, _ := sess.GetSessionVariable(ctx, dsess.AwsCredsFile) 194 credsFileStr, isStr := credsFile.(string) 195 if isStr && len(credsFileStr) > 0 { 196 params[dbfactory.AWSCredsFileParam] = credsFileStr 197 } 198 199 credsProfile, err := sess.GetSessionVariable(ctx, dsess.AwsCredsProfile) 200 profStr, isStr := credsProfile.(string) 201 if isStr && len(profStr) > 0 { 202 params[dbfactory.AWSCredsProfile] = profStr 203 } 204 205 credsRegion, err := sess.GetSessionVariable(ctx, dsess.AwsCredsRegion) 206 regionStr, isStr := credsRegion.(string) 207 if isStr && len(regionStr) > 0 { 208 params[dbfactory.AWSRegionParam] = regionStr 209 } 210 211 b := env.NewRemote("__temp__", backupUrl, params) 212 213 return syncRoots(ctx, dbData, sess, b) 214 } 215 216 func syncBackupViaName(ctx *sql.Context, dbData env.DbData, sess *dsess.DoltSession, apr *argparser.ArgParseResults) error { 217 if apr.NArg() != 2 { 218 return fmt.Errorf("usage: dolt_backup('sync', BACKUP_NAME)") 219 } 220 221 backupName := strings.TrimSpace(apr.Arg(1)) 222 backups, err := dbData.Rsr.GetBackups() 223 if err != nil { 224 return err 225 } 226 227 b, ok := backups.Get(backupName) 228 if !ok { 229 return fmt.Errorf("error: unknown backup: '%s'; %v", backupName, backups) 230 } 231 232 return syncRoots(ctx, dbData, sess, b) 233 } 234 235 func syncRoots(ctx *sql.Context, dbData env.DbData, sess *dsess.DoltSession, backup env.Remote) error { 236 destDb, err := sess.Provider().GetRemoteDB(ctx, dbData.Ddb.ValueReadWriter().Format(), backup, true) 237 if err != nil { 238 return fmt.Errorf("error loading backup destination: %w", err) 239 } 240 241 tmpDir, err := dbData.Rsw.TempTableFilesDir() 242 if err != nil { 243 return err 244 } 245 246 err = actions.SyncRoots(ctx, dbData.Ddb, destDb, tmpDir, runProgFuncs, stopProgFuncs) 247 if err != nil && err != pull.ErrDBUpToDate { 248 return fmt.Errorf("error syncing backup: %w", err) 249 } 250 251 return nil 252 }