Skip to content

Commit aee017b

Browse files
authored
timestamp handling (#13)
1 parent 42b3365 commit aee017b

File tree

4 files changed

+223
-92
lines changed

4 files changed

+223
-92
lines changed

domain/domain.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"crypto/rand"
55
"crypto/sha256"
66
"encoding/hex"
7+
"errors"
78
"fmt"
9+
"strings"
810
)
911

1012
func GenerateRandomKey() string {
@@ -24,3 +26,15 @@ func DeriveDownloadKey(uploadKey string) (string, error) {
2426
hash := sha256.Sum256([]byte(uploadKey))
2527
return hex.EncodeToString(hash[:]), nil
2628
}
29+
30+
func ValidateUploadKey(uploadKey string) error {
31+
uploadKey = strings.ToLower(uploadKey)
32+
decoded, err := hex.DecodeString(uploadKey)
33+
if err != nil {
34+
return errors.New("uploadKey must be a 256 bit hex string")
35+
}
36+
if len(decoded) != 32 {
37+
return errors.New("uploadKey must be a 256 bit hex string")
38+
}
39+
return nil
40+
}

httphandler/datahandling.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package httphandler
2+
3+
import (
4+
"strings"
5+
"time"
6+
)
7+
8+
func collectParams(params map[string][]string) map[string]string {
9+
paramMap := make(map[string]string)
10+
for key, values := range params {
11+
if len(values) > 0 {
12+
sanitizedValue := sanitizeInput(values[0])
13+
paramMap[key] = sanitizedValue
14+
}
15+
}
16+
return paramMap
17+
}
18+
19+
func addTimestampToThisData(paramMap map[string]string, path string) {
20+
// if using patch and path is empty than add a timestamp with the value suffix
21+
if path == "" {
22+
allKeys := make([]string, 0, len(paramMap))
23+
for k := range paramMap {
24+
allKeys = append(allKeys, k)
25+
}
26+
// add a timestamp with the value suffix for all keys
27+
timestamp := time.Now().UTC().Format(time.RFC3339)
28+
for _, k := range allKeys {
29+
paramMap[k+"_timestamp"] = timestamp
30+
}
31+
}
32+
33+
timestamp := time.Now().UTC().Format(time.RFC3339)
34+
paramMap["timestamp"] = timestamp
35+
}
36+
37+
func (c Config) modifyData(downloadKey string, paramMap map[string]string, path string, isPatch bool) (map[string]interface{}, error) {
38+
var dataToStore map[string]interface{}
39+
40+
if isPatch {
41+
existingData, err := c.StorageInstance.Retrieve(downloadKey)
42+
if err != nil {
43+
return nil, err
44+
}
45+
mergeData(existingData, paramMap, strings.Split(path, "/"))
46+
dataToStore = existingData
47+
} else {
48+
dataToStore = make(map[string]interface{})
49+
for k, v := range paramMap {
50+
dataToStore[k] = v
51+
}
52+
}
53+
54+
// add timestamp so that root level timestamp is always the latest of any updated value
55+
timestamp := time.Now().UTC().Format(time.RFC3339)
56+
dataToStore["timestamp"] = timestamp
57+
58+
return dataToStore, nil
59+
}
60+
61+
func mergeData(existingData map[string]interface{}, newData map[string]string, path []string) {
62+
if len(path) == 0 || (len(path) == 1 && path[0] == "") {
63+
for k, v := range newData {
64+
existingData[k] = v
65+
}
66+
return
67+
}
68+
69+
currentKey := path[0]
70+
if _, exists := existingData[currentKey]; !exists {
71+
existingData[currentKey] = make(map[string]interface{})
72+
}
73+
74+
if nestedMap, ok := existingData[currentKey].(map[string]interface{}); ok {
75+
mergeData(nestedMap, newData, path[1:])
76+
} else {
77+
existingData[currentKey] = newData
78+
}
79+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package httphandler
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestMergeData(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
existingData map[string]interface{}
12+
newData map[string]string
13+
path []string
14+
expectedData map[string]interface{}
15+
}{
16+
{
17+
name: "Empty path",
18+
existingData: map[string]interface{}{
19+
"key1": "value1",
20+
},
21+
newData: map[string]string{
22+
"key2": "value2",
23+
},
24+
path: []string{},
25+
expectedData: map[string]interface{}{
26+
"key1": "value1",
27+
"key2": "value2",
28+
},
29+
},
30+
{
31+
name: "Single empty string in path",
32+
existingData: map[string]interface{}{
33+
"key1": "value1",
34+
},
35+
newData: map[string]string{
36+
"key2": "value2",
37+
},
38+
path: []string{""},
39+
expectedData: map[string]interface{}{
40+
"key1": "value1",
41+
"key2": "value2",
42+
},
43+
},
44+
{
45+
name: "Nested merge",
46+
existingData: map[string]interface{}{
47+
"key1": map[string]interface{}{
48+
"subkey1": "subvalue1",
49+
},
50+
},
51+
newData: map[string]string{
52+
"subkey2": "subvalue2",
53+
},
54+
path: []string{"key1"},
55+
expectedData: map[string]interface{}{
56+
"key1": map[string]interface{}{
57+
"subkey1": "subvalue1",
58+
"subkey2": "subvalue2",
59+
},
60+
},
61+
},
62+
{
63+
name: "Override non-map value with new data",
64+
existingData: map[string]interface{}{
65+
"key1": "value1",
66+
},
67+
newData: map[string]string{
68+
"subkey1": "subvalue1",
69+
},
70+
path: []string{"key1"},
71+
expectedData: map[string]interface{}{
72+
"key1": map[string]string{
73+
"subkey1": "subvalue1",
74+
},
75+
},
76+
},
77+
{
78+
name: "Deeply nested merge",
79+
existingData: map[string]interface{}{
80+
"key1": map[string]interface{}{
81+
"subkey1": map[string]interface{}{
82+
"subsubkey1": "subsubvalue1",
83+
},
84+
},
85+
},
86+
newData: map[string]string{
87+
"subsubkey2": "subsubvalue2",
88+
},
89+
path: []string{"key1", "subkey1"},
90+
expectedData: map[string]interface{}{
91+
"key1": map[string]interface{}{
92+
"subkey1": map[string]interface{}{
93+
"subsubkey1": "subsubvalue1",
94+
"subsubkey2": "subsubvalue2",
95+
},
96+
},
97+
},
98+
},
99+
{
100+
name: "Create new nested map",
101+
existingData: map[string]interface{}{
102+
"key1": "value1",
103+
},
104+
newData: map[string]string{
105+
"subkey1": "subvalue1",
106+
},
107+
path: []string{"key2"},
108+
expectedData: map[string]interface{}{
109+
"key1": "value1",
110+
"key2": map[string]interface{}{
111+
"subkey1": "subvalue1",
112+
},
113+
},
114+
},
115+
}
116+
117+
for _, tt := range tests {
118+
t.Run(tt.name, func(t *testing.T) {
119+
mergeData(tt.existingData, tt.newData, tt.path)
120+
if !reflect.DeepEqual(tt.existingData, tt.expectedData) {
121+
t.Errorf("mergeData() = %v, want %v", tt.existingData, tt.expectedData)
122+
}
123+
})
124+
}
125+
}

httphandler/uploadHandler.go

Lines changed: 5 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
package httphandler
22

33
import (
4-
"encoding/hex"
5-
"errors"
64
"fmt"
75
"net/http"
8-
"strings"
9-
"time"
106

117
"github.com/dhcgn/iot-ephemeral-value-store/domain"
128
"github.com/gorilla/mux"
@@ -29,7 +25,7 @@ func (c Config) UploadHandler(w http.ResponseWriter, r *http.Request) {
2925

3026
func (c Config) handleUpload(w http.ResponseWriter, r *http.Request, uploadKey, path string, isPatch bool) {
3127
// Validate upload key
32-
if err := ValidateUploadKey(uploadKey); err != nil {
28+
if err := domain.ValidateUploadKey(uploadKey); err != nil {
3329
http.Error(w, err.Error(), http.StatusBadRequest)
3430
return
3531
}
@@ -45,71 +41,20 @@ func (c Config) handleUpload(w http.ResponseWriter, r *http.Request, uploadKey,
4541
paramMap := collectParams(r.URL.Query())
4642

4743
// Add timestamp to params
48-
addTimestamp(paramMap, path)
44+
addTimestampToThisData(paramMap, path)
4945

5046
// Handle data storage
51-
if err := c.handleDataStorage(downloadKey, paramMap, path, isPatch); err != nil {
47+
data, err := c.modifyData(downloadKey, paramMap, path, isPatch)
48+
if err != nil {
5249
http.Error(w, err.Error(), http.StatusInternalServerError)
5350
return
5451
}
52+
c.StorageInstance.Store(downloadKey, data)
5553

5654
// Construct and return response
5755
constructAndReturnResponse(w, r, downloadKey, paramMap)
5856
}
5957

60-
func collectParams(params map[string][]string) map[string]string {
61-
paramMap := make(map[string]string)
62-
for key, values := range params {
63-
if len(values) > 0 {
64-
sanitizedValue := sanitizeInput(values[0])
65-
paramMap[key] = sanitizedValue
66-
}
67-
}
68-
return paramMap
69-
}
70-
71-
func addTimestamp(paramMap map[string]string, path string) {
72-
// if using patch and path is empty than add a timestamp with the value suffix
73-
if path == "" {
74-
allKeys := make([]string, 0, len(paramMap))
75-
for k := range paramMap {
76-
allKeys = append(allKeys, k)
77-
}
78-
// add a timestamp with the value suffix for all keys
79-
timestamp := time.Now().UTC().Format(time.RFC3339)
80-
for _, k := range allKeys {
81-
paramMap[k+"_timestamp"] = timestamp
82-
}
83-
}
84-
85-
timestamp := time.Now().UTC().Format(time.RFC3339)
86-
paramMap["timestamp"] = timestamp
87-
}
88-
89-
func (c Config) handleDataStorage(downloadKey string, paramMap map[string]string, path string, isPatch bool) error {
90-
var dataToStore map[string]interface{}
91-
92-
if isPatch {
93-
existingData, err := c.StorageInstance.Retrieve(downloadKey)
94-
if err != nil {
95-
return err
96-
}
97-
mergeData(existingData, paramMap, strings.Split(path, "/"))
98-
dataToStore = existingData
99-
} else {
100-
dataToStore = make(map[string]interface{})
101-
for k, v := range paramMap {
102-
dataToStore[k] = v
103-
}
104-
}
105-
106-
// add timestamp so that root level timestamp is always the latest of any updated value
107-
timestamp := time.Now().UTC().Format(time.RFC3339)
108-
dataToStore["timestamp"] = timestamp
109-
110-
return c.StorageInstance.Store(downloadKey, dataToStore)
111-
}
112-
11358
func constructAndReturnResponse(w http.ResponseWriter, r *http.Request, downloadKey string, params map[string]string) {
11459
urls := make(map[string]string)
11560
for key := range params {
@@ -124,35 +69,3 @@ func constructAndReturnResponse(w http.ResponseWriter, r *http.Request, download
12469
"parameter_urls": urls,
12570
})
12671
}
127-
128-
func mergeData(existingData map[string]interface{}, newData map[string]string, path []string) {
129-
if len(path) == 0 || (len(path) == 1 && path[0] == "") {
130-
for k, v := range newData {
131-
existingData[k] = v
132-
}
133-
return
134-
}
135-
136-
currentKey := path[0]
137-
if _, exists := existingData[currentKey]; !exists {
138-
existingData[currentKey] = make(map[string]interface{})
139-
}
140-
141-
if nestedMap, ok := existingData[currentKey].(map[string]interface{}); ok {
142-
mergeData(nestedMap, newData, path[1:])
143-
} else {
144-
existingData[currentKey] = newData
145-
}
146-
}
147-
148-
func ValidateUploadKey(uploadKey string) error {
149-
uploadKey = strings.ToLower(uploadKey)
150-
decoded, err := hex.DecodeString(uploadKey)
151-
if err != nil {
152-
return errors.New("uploadKey must be a 256 bit hex string")
153-
}
154-
if len(decoded) != 32 {
155-
return errors.New("uploadKey must be a 256 bit hex string")
156-
}
157-
return nil
158-
}

0 commit comments

Comments
 (0)