github.com/adowair/kvdb@v0.0.0-20231101174258-ceca93b03596/kv/util.go (about) 1 package kv 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io/fs" 8 "os" 9 "path/filepath" 10 "strconv" 11 "time" 12 13 "github.com/gofrs/flock" 14 ) 15 16 // ErrNotExist is returned when attempting to perform an operation on a key 17 // which does not exist. 18 var ErrNotExist = errors.New("key does not exist") 19 20 // ErrBadFormat is returned when a read file contains data which cannot be 21 // properly parsed into an Entry. 22 var ErrBadFormat = errors.New("malformed data") 23 24 type Entry struct { 25 FirstEdited time.Time 26 LastEdited time.Time 27 Value string 28 } 29 30 // delimiter is the internal symbol used to separate fields of an Entry 31 // in its encoded, on-disk format. 32 const delimiter = ',' 33 34 // read is a wrapper around readFrom() which reads a key 35 // from the current directory. 36 func read(key string) (*Entry, error) { 37 wd, err := os.Getwd() 38 if err != nil { 39 return nil, err 40 } 41 42 path := filepath.Join(wd, key) 43 return readFrom(path) 44 } 45 46 // readFrom is an internal function that gets the contents of the file at path, 47 // and parses it into an Entry if possible. The function takes an exclusive 48 // advisory lock on the file, on which other kvdb instances will block. 49 func readFrom(path string) (*Entry, error) { 50 if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { 51 return nil, ErrNotExist 52 } else if err != nil { 53 return nil, err 54 } 55 56 lock := flock.New(path) 57 err := lock.Lock() 58 defer lock.Unlock() 59 if err != nil { 60 return nil, err 61 } 62 63 b, err := os.ReadFile(path) 64 if err != nil { 65 return nil, err 66 } 67 68 raw := bytes.SplitN(b, []byte{delimiter}, 3) 69 if len(raw) != 3 { 70 return nil, fmt.Errorf("%w, missing value or timestamp %q", ErrBadFormat, b) 71 } 72 73 firstEdited, err := strconv.ParseInt(string(raw[0]), 10, 64) 74 if err != nil { 75 return nil, fmt.Errorf("%w, could not parse first-set timestamp %q, %w", 76 ErrBadFormat, raw[0], err) 77 } 78 79 lastEdited, err := strconv.ParseInt(string(raw[1]), 10, 64) 80 if err != nil { 81 return nil, fmt.Errorf("%w, could not parse last-set timestamp %q, %w", 82 ErrBadFormat, raw[1], err) 83 } 84 85 value := raw[2] 86 if len(value) == 0 { 87 return nil, fmt.Errorf("%w, missing value %q", ErrBadFormat, b) 88 } 89 90 return &Entry{ 91 FirstEdited: time.Unix(firstEdited, 0), 92 LastEdited: time.Unix(lastEdited, 0), 93 Value: string(value), 94 }, nil 95 } 96 97 // write is a wrapper around writeTo() which writes a key to the current 98 // directory. 99 func write(key string, entry *Entry) error { 100 wd, err := os.Getwd() 101 if err != nil { 102 return err 103 } 104 105 path := filepath.Join(wd, key) 106 return writeTo(path, entry) 107 } 108 109 // writeTo is an internal function that serializes an Entry, and then 110 // writes it to the given path. The function takes an exclusive 111 // advisory lock on the file, on which other kvdb instances will block. 112 func writeTo(path string, entry *Entry) error { 113 if len(entry.Value) == 0 { 114 return fmt.Errorf("%w, storing empty \"\" is not allowed", ErrBadFormat) 115 } 116 117 data := fmt.Sprintf( 118 "%d%c%d%c%s", 119 entry.FirstEdited.Unix(), 120 delimiter, 121 entry.LastEdited.Unix(), 122 delimiter, 123 entry.Value) 124 125 lock := flock.New(path) 126 err := lock.Lock() 127 defer lock.Unlock() 128 if err != nil { 129 return err 130 } 131 132 return os.WriteFile(path, []byte(data), os.ModePerm) 133 } 134 135 func delete(key string) error { 136 wd, err := os.Getwd() 137 if err != nil { 138 return err 139 } 140 141 path := filepath.Join(wd, key) 142 return deleteFile(path) 143 } 144 145 func deleteFile(path string) error { 146 lock := flock.New(path) 147 err := lock.Lock() 148 defer lock.Unlock() 149 if err != nil { 150 return err 151 } 152 153 err = os.Remove(path) 154 if errors.Is(err, fs.ErrNotExist) { 155 return nil 156 } 157 158 return err 159 }