github.com/quickfeed/quickfeed@v0.0.0-20240507093252-ed8ca812a09c/internal/env/save.go (about) 1 package env 2 3 import ( 4 "fmt" 5 "os" 6 "strings" 7 ) 8 9 // Prepared returns nil if the given env file exists and the corresponding backup file does not. 10 // Otherwise, it returns an error. 11 // 12 // If the env file does not exist, a MissingError returned. 13 // QuickFeed requires the env file to load (some) existing environment variables, 14 // even when creating a new GitHub app, and overwriting some environment variables. 15 // If the backup file exists, an ExistsError is returned. 16 // This is to avoid that QuickFeed overwrites an existing backup file. 17 func Prepared(filename string) error { 18 bakFilename := filename + ".bak" 19 if exists(bakFilename) { 20 return ExistsError(bakFilename) 21 } 22 if !exists(filename) { 23 return MissingError(filename) 24 } 25 return nil 26 } 27 28 // Save writes the given environment variables to the given file, 29 // replacing or leaving behind existing variables. 30 // 31 // If the file exists, it will be updated, but leaving a backup file. 32 // If a backup already exists it will be removed first. 33 func Save(filename string, env map[string]string) error { 34 // Load the existing file's content before renaming it. 35 content := load(filename) 36 bakFilename := filename + ".bak" 37 if exists(bakFilename) { 38 if err := os.Remove(bakFilename); err != nil { 39 return err 40 } 41 } 42 if err := os.Rename(filename, bakFilename); err != nil { 43 return err 44 } 45 // Update the file with new environment variables. 46 return update(filename, content, env) 47 } 48 49 // load reads the content of the given file assuming it exists. 50 // An empty string is returned if the file does not exist. 51 func load(filename string) string { 52 content, err := os.ReadFile(filename) 53 if err != nil { 54 return "" 55 } 56 return string(content) 57 } 58 59 // update updates the file's content with the provided environment variables. 60 func update(filename, content string, env map[string]string) error { 61 file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) 62 if err != nil { 63 return err 64 } 65 66 // Map of updated environment variables 67 updated := make(map[string]bool) 68 69 // Scan existing file's content and update existing environment variables. 70 for _, line := range strings.Split(content, "\n") { 71 key, val, found := strings.Cut(line, "=") 72 if !found { 73 // Leave non-environment and blank lines unchanged. 74 fmt.Fprintln(file, line) 75 continue 76 } 77 // Remove spaces around the key and value, if any. 78 key, val = strings.TrimSpace(key), strings.TrimSpace(val) 79 if v, ok := env[key]; ok { 80 // Replace old value with new value. 81 val = v 82 } 83 fmt.Fprintf(file, "%s=%s\n", key, val) 84 updated[key] = true 85 } 86 87 // Write new lines for any new environment variables. 88 for key, val := range env { 89 if _, ok := updated[key]; ok { 90 continue 91 } 92 if _, err = fmt.Fprintf(file, "%s=%s\n", key, val); err != nil { 93 return err 94 } 95 } 96 return file.Close() 97 } 98 99 func exists(filename string) bool { 100 _, err := os.Stat(filename) 101 return err == nil 102 } 103 104 type backupExistsError struct { 105 filename string 106 } 107 108 func ExistsError(filename string) error { 109 return backupExistsError{filename: filename} 110 } 111 112 func (e backupExistsError) Error() string { 113 return fmt.Sprintf("%s already exists; check its content before removing and try again", e.filename) 114 } 115 116 type missingEnvError struct { 117 filename string 118 } 119 120 func MissingError(filename string) error { 121 return missingEnvError{filename: filename} 122 } 123 124 func (e missingEnvError) Error() string { 125 return fmt.Sprintf("missing required %q file", e.filename) 126 }