Skip to content

Commit 28c6ab9

Browse files
authored
Feature/file cache (#12)
- implement cache
1 parent 9df1518 commit 28c6ab9

File tree

10 files changed

+310
-290
lines changed

10 files changed

+310
-290
lines changed

emmer.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func main() {
2525
// api
2626
http.HandleFunc("/ping", server.PingHandler)
2727
http.HandleFunc("/logs", server.Auth(server.LogsHandler))
28+
http.HandleFunc("/commit", server.Auth(server.CommitHandler))
2829
http.HandleFunc("/api/", server.Auth(server.ApiHandler))
2930

3031
// start the server

server/api.go

Lines changed: 47 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ package server
33
import (
44
"crypto/rand"
55
"encoding/base64"
6-
"encoding/json"
76
"errors"
8-
"strings"
7+
"strconv"
98

109
emmerFs "github.com/TimoKats/emmer/server/fs"
1110

@@ -16,67 +15,27 @@ import (
1615
"os"
1716
)
1817

19-
var config Config
18+
var session Session
2019

21-
// get HTTP request and format it into Request object used by server
22-
func parseRequest(r *http.Request) (Request, error) {
23-
// parse URL path (parameters, path)
24-
request := Request{Method: r.Method, Mode: r.FormValue("mode")}
25-
urlPath := r.URL.Path[len("/api/"):]
26-
urlItems := strings.Split(urlPath, "/")
27-
if len(urlItems) > 0 {
28-
request.Table = urlItems[0]
29-
if len(urlItems) > 1 {
30-
request.Key = urlItems[1:]
31-
}
32-
}
33-
// parse request body
34-
payload, err := io.ReadAll(r.Body)
35-
defer r.Body.Close() //nolint:errcheck
36-
if err != nil {
37-
return request, err
38-
}
39-
if len(payload) > 0 {
40-
err = json.Unmarshal(payload, &request.Value)
41-
}
42-
return request, err
43-
}
20+
// helper function that selects the interface based on the URL path
21+
func ApiHandler(w http.ResponseWriter, r *http.Request) {
4422

45-
// takes response object and writes the HTTP response object
46-
func parseResponse(w http.ResponseWriter, response Response) error {
47-
if response.Error != nil {
48-
w.Header().Set("Content-Type", "text/plain")
49-
if strings.Contains(response.Error.Error(), "not found") {
50-
w.WriteHeader(404)
51-
} else {
52-
w.WriteHeader(500)
23+
// returns the item to apply CRUD operations on
24+
toggle := func(request Request) (Item, error) {
25+
if len(request.Key) > 0 {
26+
return EntryItem{}, nil
5327
}
54-
return json.NewEncoder(w).Encode(response.Error.Error())
55-
}
56-
w.Header().Set("Content-Type", "application/json")
57-
w.WriteHeader(200)
58-
return json.NewEncoder(w).Encode(response.Data)
59-
}
60-
61-
// returns the item to apply CRUD operations on
62-
func selectItem(request Request) (Item, error) {
63-
if len(request.Key) > 0 {
64-
return EntryItem{}, nil
28+
return TableItem{}, nil
6529
}
66-
return TableItem{}, nil
67-
}
6830

69-
// helper function that selects the interface based on the URL path
70-
func ApiHandler(w http.ResponseWriter, r *http.Request) {
71-
// set up
31+
// apply CRUD to correct item and return response
7232
var response Response
7333
request, parseErr := parseRequest(r)
74-
item, itemErr := selectItem(request)
34+
item, itemErr := toggle(request)
7535
if err := errors.Join(parseErr, itemErr); err != nil {
7636
http.Error(w, err.Error(), http.StatusBadRequest) // to response
7737
return
7838
}
79-
// select function
8039
switch request.Method {
8140
case "PUT":
8241
response = item.Add(request)
@@ -85,33 +44,44 @@ func ApiHandler(w http.ResponseWriter, r *http.Request) {
8544
case "GET":
8645
response = item.Get(request)
8746
default:
88-
http.Error(w, "please use put/del/get", http.StatusMethodNotAllowed) // to response
47+
http.Error(w, "please use put/del/get", http.StatusMethodNotAllowed)
8948
return
9049
}
91-
// check errors and return response
9250
if err := parseResponse(w, response); err != nil {
9351
http.Error(w, err.Error(), http.StatusInternalServerError)
9452
return
9553
}
9654
}
9755

98-
// does nothing. Only used for health checks
56+
// does nothing, only used for health checks
9957
func PingHandler(w http.ResponseWriter, r *http.Request) {
10058
fmt.Fprintln(w, "pong") //nolint:errcheck
10159
}
10260

10361
// shows last n (20) logs from server
10462
func LogsHandler(w http.ResponseWriter, r *http.Request) {
105-
for _, entry := range config.logBuffer.GetLogs() {
63+
for _, entry := range session.logBuffer.GetLogs() {
10664
fmt.Fprint(w, entry) //nolint:errcheck
10765
}
10866
}
10967

68+
// write all cache to filesystem
69+
func CommitHandler(w http.ResponseWriter, r *http.Request) {
70+
for filename, data := range session.cache.data {
71+
err := session.fs.Put(filename, data)
72+
if err != nil {
73+
log.Printf("error writing cache of %s", filename)
74+
} else {
75+
fmt.Fprint(w, "cache written to filesystem") //nolint:errcheck
76+
}
77+
}
78+
}
79+
11080
// basic auth that uses public username/password for check
11181
func Auth(next http.HandlerFunc) http.HandlerFunc {
11282
return func(w http.ResponseWriter, r *http.Request) {
11383
user, pass, ok := r.BasicAuth()
114-
if !ok || user != config.username || pass != config.password {
84+
if !ok || user != session.config.username || pass != session.config.password {
11585
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
11686
http.Error(w, "Unauthorized", http.StatusUnauthorized)
11787
return
@@ -135,15 +105,29 @@ func init() {
135105
password = base64.URLEncoding.EncodeToString(b)
136106
log.Printf("set password to: %s", password)
137107
}
108+
// cache settings
109+
commit := 1
110+
commitEnv := os.Getenv("EM_COMMIT")
111+
if commitEnv != "" {
112+
commitInt, err := strconv.Atoi(commitEnv)
113+
if err != nil {
114+
fmt.Printf("Error converting commit strategy to int: %v", err)
115+
return
116+
}
117+
commit = commitInt
118+
}
138119
// logs settings
139120
buffer := NewLogBuffer(20)
140121
log.SetOutput(io.MultiWriter(os.Stdout, buffer))
141122
// create config object
142-
config = Config{
143-
logBuffer: buffer,
144-
autoTable: os.Getenv("EM_AUTOTABLE") != "false",
145-
username: username,
146-
password: password,
147-
fs: emmerFs.SetupLocal(),
123+
session.config = Config{
124+
username: username,
125+
password: password,
126+
commit: commit,
148127
}
128+
session.logBuffer = buffer
129+
session.cache.data = make(map[string]map[string]any)
130+
session.cache.data = make(map[string]map[string]any)
131+
session.fs = emmerFs.SetupLocal()
132+
session.commits = 1
149133
}

server/entry.go

Lines changed: 25 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,55 @@
11
package server
22

33
import (
4-
"errors"
54
"log"
6-
"strconv"
75
)
86

97
type EntryItem struct{}
108

11-
// used to query on multi-keys. E.g. [1,2,3] returns map[1,2,3] > value
12-
func findKey(data map[string]any, key []string) (any, error) {
13-
var current any = data
14-
if len(key) == 0 || key[0] == "" {
15-
return data, nil
16-
}
17-
for _, step := range key {
18-
switch typed := current.(type) {
19-
case map[string]any:
20-
val, ok := typed[step]
21-
if !ok {
22-
return nil, errors.New("key " + step + " not found in map")
23-
}
24-
current = val
25-
case []any:
26-
index, err := strconv.Atoi(step)
27-
if err != nil {
28-
return nil, errors.New("invalid index " + step + " for list")
29-
}
30-
if index < 0 || index >= len(typed) {
31-
return nil, errors.New("index " + step + " out of bounds")
32-
}
33-
current = typed[index]
34-
default:
35-
return nil, errors.New("cannot descend into type")
36-
}
37-
}
38-
return current, nil
39-
}
40-
419
// fetches path for table name, then removes key from JSON.
4210
func (EntryItem) Del(request Request) Response {
4311
log.Printf("deleting key %s in %v", request.Key, request.Table)
44-
if _, err := config.fs.Fetch(request.Table); err != nil {
12+
// read file from cache/fs
13+
data, err := read(request.Table, request.Mode)
14+
if err != nil {
15+
return Response{Data: nil, Error: err}
16+
}
17+
// update contents, and write to cache/fs
18+
if err = pop(data, request.Key); err != nil {
19+
return Response{Data: nil, Error: err}
20+
}
21+
if err = write(request.Table, data); err != nil {
4522
return Response{Data: nil, Error: err}
4623
}
47-
err := config.fs.DeleteJSON(request.Table, request.Key)
4824
return Response{Data: "deleted key in " + request.Table, Error: err}
4925
}
5026

5127
// parses entry payload and updates the corresponding table
5228
func (EntryItem) Add(request Request) Response {
5329
log.Printf("adding value for %s in table %s", request.Key, request.Table)
5430
// if it doesn't exist, create it. still errors? return error.
55-
if _, err := config.fs.Fetch(request.Table); err != nil {
56-
if config.autoTable {
57-
err = config.fs.CreateJSON(request.Table, nil)
58-
}
59-
if err != nil {
60-
return Response{Data: nil, Error: err}
61-
}
31+
data, err := read(request.Table, request.Mode)
32+
if err != nil {
33+
return Response{Data: nil, Error: err}
34+
}
35+
// update json, and update cache
36+
err = insert(data, request.Key, request.Value, request.Mode)
37+
if err != nil {
38+
return Response{Data: nil, Error: err}
39+
}
40+
if err = write(request.Table, data); err != nil {
41+
return Response{Data: nil, Error: err}
6242
}
63-
// update json file with new values
64-
err := config.fs.UpdateJSON(request.Table, request.Key, request.Value, request.Mode)
6543
return Response{Data: "added key in " + request.Table, Error: err}
6644
}
6745

68-
// query for an entry in a table. Returns query result.
46+
// query for an entry in a table. Returns query result (and updates cache).
6947
func (EntryItem) Get(request Request) Response {
70-
log.Printf("querying table %s", request.Table)
71-
// get complete json data
72-
data, err := config.fs.ReadJSON(request.Table)
48+
log.Printf("querying table: %s", request.Table)
49+
data, err := read(request.Table, request.Mode)
7350
if err != nil {
7451
return Response{Data: nil, Error: err}
7552
}
76-
// filter json data
77-
result, err := findKey(data, request.Key)
53+
result, err := query(data, request.Key)
7854
return Response{Data: result, Error: err}
7955
}

0 commit comments

Comments
 (0)