github.com/omniscale/go-osm@v0.3.1/state/state.go (about)

     1  package state
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  )
    13  
    14  type DiffState struct {
    15  	Time     time.Time
    16  	Sequence int
    17  	URL      string
    18  }
    19  
    20  func (d DiffState) write(w io.Writer) error {
    21  	lines := []string{}
    22  	lines = append(lines, "timestamp="+d.Time.Format(timestampFormat))
    23  	if d.Sequence != 0 {
    24  		lines = append(lines, "sequenceNumber="+fmt.Sprintf("%d", d.Sequence))
    25  	}
    26  	lines = append(lines, "replicationUrl="+d.URL)
    27  
    28  	for _, line := range lines {
    29  		_, err := w.Write([]byte(line + "\n"))
    30  		if err != nil {
    31  			return err
    32  		}
    33  	}
    34  	return nil
    35  }
    36  
    37  func WriteFile(filename string, state *DiffState) error {
    38  	tmpname := filename + "~"
    39  	f, err := os.Create(tmpname)
    40  	if err != nil {
    41  		return fmt.Errorf("creating temp file for writing state file: %w", err)
    42  	}
    43  	err = state.write(f)
    44  	if err != nil {
    45  		f.Close()
    46  		os.Remove(tmpname)
    47  		return fmt.Errorf("writing state to %q: %w", tmpname, err)
    48  	}
    49  	f.Close()
    50  	return os.Rename(tmpname, filename)
    51  }
    52  
    53  func ParseFile(stateFile string) (*DiffState, error) {
    54  	f, err := os.Open(stateFile)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	defer f.Close()
    59  	return Parse(f)
    60  }
    61  
    62  // Parse parses an INI style state.txt file.
    63  // timestamp is required, sequenceNumber and replicationUrl can be empty.
    64  func Parse(f io.Reader) (*DiffState, error) {
    65  	values, err := parseSimpleIni(f)
    66  	if err != nil {
    67  		return nil, fmt.Errorf("parsing state file as INI: %w", err)
    68  	}
    69  
    70  	timestamp, err := parseTimeStamp(values["timestamp"])
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	sequence, err := parseSequence(values["sequenceNumber"])
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	url := values["replicationUrl"]
    80  	return &DiffState{
    81  		Time:     timestamp,
    82  		Sequence: sequence,
    83  		URL:      url,
    84  	}, nil
    85  }
    86  
    87  func parseSimpleIni(f io.Reader) (map[string]string, error) {
    88  	result := make(map[string]string)
    89  
    90  	reader := bufio.NewScanner(f)
    91  	for reader.Scan() {
    92  		line := reader.Text()
    93  		if line != "" && line[0] == '#' {
    94  			continue
    95  		}
    96  		if strings.Contains(line, "=") {
    97  			keyVal := strings.SplitN(line, "=", 2)
    98  			result[strings.TrimSpace(keyVal[0])] = strings.TrimSpace(keyVal[1])
    99  		}
   100  
   101  	}
   102  	if err := reader.Err(); err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	return result, nil
   107  }
   108  
   109  const timestampFormat = "2006-01-02T15\\:04\\:05Z"
   110  
   111  func parseTimeStamp(value string) (time.Time, error) {
   112  	if value == "" {
   113  		return time.Time{}, errors.New("missing timestamp in state")
   114  	}
   115  	return time.Parse(timestampFormat, value)
   116  }
   117  
   118  func parseSequence(value string) (int, error) {
   119  	if value == "" {
   120  		return 0, nil
   121  	}
   122  	val, err := strconv.ParseInt(value, 10, 32)
   123  	return int(val), err
   124  }