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