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  }