github.com/cloudberrydb/gpbackup@v1.0.3-0.20240118031043-5410fd45eed6/utils/util.go (about)

     1  package utils
     2  
     3  /*
     4   * This file contains miscellaneous functions that are generally useful and
     5   * don't fit into any other file.
     6   */
     7  
     8  import (
     9  	"crypto/sha256"
    10  	"fmt"
    11  	"os"
    12  	"os/exec"
    13  	"os/signal"
    14  	path "path/filepath"
    15  	"regexp"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/cloudberrydb/gp-common-go-libs/dbconn"
    20  	"github.com/cloudberrydb/gp-common-go-libs/gplog"
    21  	"github.com/cloudberrydb/gp-common-go-libs/operating"
    22  	"github.com/cloudberrydb/gpbackup/filepath"
    23  	"github.com/pkg/errors"
    24  	"golang.org/x/sys/unix"
    25  )
    26  
    27  const MINIMUM_GPDB4_VERSION = "1.1.0"
    28  const MINIMUM_GPDB5_VERSION = "1.1.0"
    29  
    30  /*
    31   * General helper functions
    32   */
    33  
    34  func CommandExists(cmd string) bool {
    35  	_, err := exec.LookPath(cmd)
    36  	return err == nil
    37  }
    38  
    39  func FileExists(filename string) bool {
    40  	_, err := os.Stat(filename)
    41  	return err == nil
    42  }
    43  
    44  func RemoveFileIfExists(filename string) error {
    45  	baseFilename := path.Base(filename)
    46  	if FileExists(filename) {
    47  		gplog.Debug("File %s: Exists, Attempting Removal", baseFilename)
    48  		err := os.Remove(filename)
    49  		if err != nil {
    50  			gplog.Error("File %s: Failed to remove. Error %s", baseFilename, err.Error())
    51  			return err
    52  		}
    53  		gplog.Debug("File %s: Successfully removed", baseFilename)
    54  	} else {
    55  		gplog.Debug("File %s: Does not exist. No removal needed", baseFilename)
    56  	}
    57  	return nil
    58  }
    59  
    60  func OpenFileForWrite(filename string) (*os.File, error) {
    61  	baseFilename := path.Base(filename)
    62  	file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
    63  	if err != nil {
    64  		gplog.Error("File %s: Failed to open. Error %s", baseFilename, err.Error())
    65  	}
    66  	return file, err
    67  }
    68  
    69  func WriteToFileAndMakeReadOnly(filename string, contents []byte) error {
    70  	baseFilename := path.Base(filename)
    71  	file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
    72  	if err != nil {
    73  		gplog.Error("File %s: Failed to open. Error %s", baseFilename, err.Error())
    74  		return err
    75  	}
    76  
    77  	_, err = file.Write(contents)
    78  	if err != nil {
    79  		gplog.Error("File %s: Could not write to file. Error %s", baseFilename, err.Error())
    80  		return err
    81  	}
    82  
    83  	err = file.Chmod(0444)
    84  	if err != nil {
    85  		gplog.Error("File %s: Could not chmod. Error %s", baseFilename, err.Error())
    86  		return err
    87  	}
    88  
    89  	err = file.Sync()
    90  	if err != nil {
    91  		gplog.Error("File %s: Could not sync. Error %s", baseFilename, err.Error())
    92  		return err
    93  	}
    94  
    95  	return file.Close()
    96  }
    97  
    98  // Dollar-quoting logic is based on appendStringLiteralDQ() in pg_dump.
    99  func DollarQuoteString(literal string) string {
   100  	delimStr := "_XXXXXXX"
   101  	quoteStr := ""
   102  	for i := range delimStr {
   103  		testStr := "$" + delimStr[0:i]
   104  		if !strings.Contains(literal, testStr) {
   105  			quoteStr = testStr + "$"
   106  			break
   107  		}
   108  	}
   109  	return quoteStr + literal + quoteStr
   110  }
   111  
   112  // This function assumes that all identifiers are already appropriately quoted
   113  func MakeFQN(schema string, object string) string {
   114  	return fmt.Sprintf("%s.%s", schema, object)
   115  }
   116  
   117  // Since we currently split schema and table on the dot (.), we can't allow
   118  // users to filter backup or restore tables with dots in the schema or table.
   119  func ValidateFQNs(tableList []string) error {
   120  	validFormat := regexp.MustCompile(`^[^.]+\.[^.]+$`)
   121  	for _, fqn := range tableList {
   122  		if !validFormat.Match([]byte(fqn)) {
   123  			return errors.Errorf(`Table "%s" is not correctly fully-qualified.  Please ensure table is in the format "schema.table" and both the schema and table does not contain a dot (.).`, fqn)
   124  		}
   125  	}
   126  
   127  	return nil
   128  }
   129  
   130  func ValidateFullPath(path string) error {
   131  	if len(path) > 0 && !(strings.HasPrefix(path, "/") || strings.HasPrefix(path, "~")) {
   132  		return errors.Errorf("%s is not an absolute path.", path)
   133  	}
   134  	return nil
   135  }
   136  
   137  // A description of compression levels for some compression type
   138  type CompressionLevelsDescription struct {
   139  	Min int
   140  	Max int
   141  }
   142  
   143  func ValidateCompressionTypeAndLevel(compressionType string, compressionLevel int) error {
   144  	compressionLevelsForType := map[string]CompressionLevelsDescription{
   145  		"gzip": {Min: 1, Max: 9},
   146  		"zstd": {Min: 1, Max: 19},
   147  	}
   148  
   149  	if levelsDescription, ok := compressionLevelsForType[compressionType]; ok {
   150  		if compressionLevel < levelsDescription.Min || compressionLevel > levelsDescription.Max {
   151  			return fmt.Errorf("compression type '%s' only allows compression levels between %d and %d, but the provided level is %d", compressionType, levelsDescription.Min, levelsDescription.Max, compressionLevel)
   152  		}
   153  	} else {
   154  		return fmt.Errorf("unknown compression type '%s'", compressionType)
   155  	}
   156  
   157  	return nil
   158  }
   159  
   160  func InitializeSignalHandler(cleanupFunc func(bool), procDesc string, termFlag *bool) {
   161  	signalChan := make(chan os.Signal, 1)
   162  	signal.Notify(signalChan, unix.SIGINT, unix.SIGTERM)
   163  	go func() {
   164  		sig := <-signalChan
   165  		fmt.Println() // Add newline after "^C" is printed
   166  		switch sig {
   167  		case unix.SIGINT:
   168  				gplog.Warn("Received an interrupt signal, aborting %s", procDesc)
   169  		case unix.SIGTERM:
   170  				gplog.Warn("Received a termination signal, aborting %s", procDesc)
   171  		}
   172  		*termFlag = true
   173  		cleanupFunc(true)
   174  		os.Exit(2)
   175  	}()
   176  }
   177  
   178  // TODO: Uniquely identify COPY commands in the multiple data file case to allow terminating sessions
   179  func TerminateHangingCopySessions(connectionPool *dbconn.DBConn, fpInfo filepath.FilePathInfo, appName string) {
   180  	var query string
   181  	copyFileName := fpInfo.GetSegmentPipePathForCopyCommand()
   182  	query = fmt.Sprintf(`SELECT
   183  	pg_terminate_backend(pid)
   184  	FROM pg_stat_activity
   185  	WHERE application_name = '%s'
   186  	AND query LIKE '%%%s%%'
   187  	AND pid <> pg_backend_pid()`, appName, copyFileName)
   188  	// We don't check the error as the connection may have finished or been previously terminated
   189  	_, _ = connectionPool.Exec(query)
   190  }
   191  
   192  func ValidateGPDBVersionCompatibility(connectionPool *dbconn.DBConn) {
   193  	
   194  }
   195  
   196  func LogExecutionTime(start time.Time, name string) {
   197  	elapsed := time.Since(start)
   198  	gplog.Debug(fmt.Sprintf("%s took %s", name, elapsed))
   199  }
   200  
   201  func Exists(slice []string, val string) bool {
   202  	for _, item := range slice {
   203  		if item == val {
   204  			return true
   205  		}
   206  	}
   207  	return false
   208  }
   209  
   210  func SchemaIsExcludedByUser(inSchemasUserInput []string, exSchemasUserInput []string, schemaName string) bool {
   211  	included := Exists(inSchemasUserInput, schemaName) || len(inSchemasUserInput) == 0
   212  	excluded := Exists(exSchemasUserInput, schemaName)
   213  	return excluded || !included
   214  }
   215  
   216  func RelationIsExcludedByUser(inRelationsUserInput []string, exRelationsUserInput []string, tableFQN string) bool {
   217  	included := Exists(inRelationsUserInput, tableFQN) || len(inRelationsUserInput) == 0
   218  	excluded := Exists(exRelationsUserInput, tableFQN)
   219  	return excluded || !included
   220  }
   221  
   222  func UnquoteIdent(ident string) string {
   223  	if len(ident) <= 1 {
   224  		return ident
   225  	}
   226  
   227  	if ident[0] == '"' && ident[len(ident)-1] == '"' {
   228  		ident = ident[1 : len(ident)-1]
   229  		unescape := strings.NewReplacer(`""`, `"`)
   230  		ident = unescape.Replace(ident)
   231  	}
   232  
   233  	return ident
   234  }
   235  
   236  func QuoteIdent(connectionPool *dbconn.DBConn, ident string) string {
   237  	return dbconn.MustSelectString(connectionPool, fmt.Sprintf(`SELECT quote_ident('%s')`, EscapeSingleQuotes(ident)))
   238  }
   239  
   240  func SliceToQuotedString(slice []string) string {
   241  	quotedStrings := make([]string, len(slice))
   242  	for i, str := range slice {
   243  		quotedStrings[i] = fmt.Sprintf("'%s'", EscapeSingleQuotes(str))
   244  	}
   245  	return strings.Join(quotedStrings, ",")
   246  }
   247  
   248  func EscapeSingleQuotes(str string) string {
   249  	return strings.Replace(str, "'", "''", -1)
   250  }
   251  
   252  func GetFileHash(filename string) ([32]byte, error) {
   253  	contents, err := operating.System.ReadFile(filename)
   254  	if err != nil {
   255  		gplog.Error("Failed to read contents of file %s: %v", filename, err)
   256  		return [32]byte{}, err
   257  	}
   258  	filehash := sha256.Sum256(contents)
   259  	if err != nil {
   260  		gplog.Error("Failed to hash contents of file %s: %v", filename, err)
   261  		return [32]byte{}, err
   262  	}
   263  	return filehash, nil
   264  }
   265