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  }