Skip to content

Commit

Permalink
timestamp handling (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhcgn authored Jun 15, 2024
1 parent 42b3365 commit aee017b
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 92 deletions.
14 changes: 14 additions & 0 deletions domain/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
)

func GenerateRandomKey() string {
Expand All @@ -24,3 +26,15 @@ func DeriveDownloadKey(uploadKey string) (string, error) {
hash := sha256.Sum256([]byte(uploadKey))
return hex.EncodeToString(hash[:]), nil
}

func ValidateUploadKey(uploadKey string) error {
uploadKey = strings.ToLower(uploadKey)
decoded, err := hex.DecodeString(uploadKey)
if err != nil {
return errors.New("uploadKey must be a 256 bit hex string")
}
if len(decoded) != 32 {
return errors.New("uploadKey must be a 256 bit hex string")
}
return nil
}
79 changes: 79 additions & 0 deletions httphandler/datahandling.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package httphandler

import (
"strings"
"time"
)

func collectParams(params map[string][]string) map[string]string {
paramMap := make(map[string]string)
for key, values := range params {
if len(values) > 0 {
sanitizedValue := sanitizeInput(values[0])
paramMap[key] = sanitizedValue
}
}
return paramMap
}

func addTimestampToThisData(paramMap map[string]string, path string) {
// if using patch and path is empty than add a timestamp with the value suffix
if path == "" {
allKeys := make([]string, 0, len(paramMap))
for k := range paramMap {
allKeys = append(allKeys, k)
}
// add a timestamp with the value suffix for all keys
timestamp := time.Now().UTC().Format(time.RFC3339)
for _, k := range allKeys {
paramMap[k+"_timestamp"] = timestamp
}
}

timestamp := time.Now().UTC().Format(time.RFC3339)
paramMap["timestamp"] = timestamp
}

func (c Config) modifyData(downloadKey string, paramMap map[string]string, path string, isPatch bool) (map[string]interface{}, error) {
var dataToStore map[string]interface{}

if isPatch {
existingData, err := c.StorageInstance.Retrieve(downloadKey)
if err != nil {
return nil, err
}
mergeData(existingData, paramMap, strings.Split(path, "/"))
dataToStore = existingData
} else {
dataToStore = make(map[string]interface{})
for k, v := range paramMap {
dataToStore[k] = v
}
}

// add timestamp so that root level timestamp is always the latest of any updated value
timestamp := time.Now().UTC().Format(time.RFC3339)
dataToStore["timestamp"] = timestamp

return dataToStore, nil
}

func mergeData(existingData map[string]interface{}, newData map[string]string, path []string) {
if len(path) == 0 || (len(path) == 1 && path[0] == "") {
for k, v := range newData {
existingData[k] = v
}
return
}

currentKey := path[0]
if _, exists := existingData[currentKey]; !exists {
existingData[currentKey] = make(map[string]interface{})
}

if nestedMap, ok := existingData[currentKey].(map[string]interface{}); ok {
mergeData(nestedMap, newData, path[1:])
} else {
existingData[currentKey] = newData
}
}
125 changes: 125 additions & 0 deletions httphandler/datahandling_mergeData_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package httphandler

import (
"reflect"
"testing"
)

func TestMergeData(t *testing.T) {
tests := []struct {
name string
existingData map[string]interface{}
newData map[string]string
path []string
expectedData map[string]interface{}
}{
{
name: "Empty path",
existingData: map[string]interface{}{
"key1": "value1",
},
newData: map[string]string{
"key2": "value2",
},
path: []string{},
expectedData: map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
},
{
name: "Single empty string in path",
existingData: map[string]interface{}{
"key1": "value1",
},
newData: map[string]string{
"key2": "value2",
},
path: []string{""},
expectedData: map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
},
{
name: "Nested merge",
existingData: map[string]interface{}{
"key1": map[string]interface{}{
"subkey1": "subvalue1",
},
},
newData: map[string]string{
"subkey2": "subvalue2",
},
path: []string{"key1"},
expectedData: map[string]interface{}{
"key1": map[string]interface{}{
"subkey1": "subvalue1",
"subkey2": "subvalue2",
},
},
},
{
name: "Override non-map value with new data",
existingData: map[string]interface{}{
"key1": "value1",
},
newData: map[string]string{
"subkey1": "subvalue1",
},
path: []string{"key1"},
expectedData: map[string]interface{}{
"key1": map[string]string{
"subkey1": "subvalue1",
},
},
},
{
name: "Deeply nested merge",
existingData: map[string]interface{}{
"key1": map[string]interface{}{
"subkey1": map[string]interface{}{
"subsubkey1": "subsubvalue1",
},
},
},
newData: map[string]string{
"subsubkey2": "subsubvalue2",
},
path: []string{"key1", "subkey1"},
expectedData: map[string]interface{}{
"key1": map[string]interface{}{
"subkey1": map[string]interface{}{
"subsubkey1": "subsubvalue1",
"subsubkey2": "subsubvalue2",
},
},
},
},
{
name: "Create new nested map",
existingData: map[string]interface{}{
"key1": "value1",
},
newData: map[string]string{
"subkey1": "subvalue1",
},
path: []string{"key2"},
expectedData: map[string]interface{}{
"key1": "value1",
"key2": map[string]interface{}{
"subkey1": "subvalue1",
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mergeData(tt.existingData, tt.newData, tt.path)
if !reflect.DeepEqual(tt.existingData, tt.expectedData) {
t.Errorf("mergeData() = %v, want %v", tt.existingData, tt.expectedData)
}
})
}
}
97 changes: 5 additions & 92 deletions httphandler/uploadHandler.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package httphandler

import (
"encoding/hex"
"errors"
"fmt"
"net/http"
"strings"
"time"

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

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

// Add timestamp to params
addTimestamp(paramMap, path)
addTimestampToThisData(paramMap, path)

// Handle data storage
if err := c.handleDataStorage(downloadKey, paramMap, path, isPatch); err != nil {
data, err := c.modifyData(downloadKey, paramMap, path, isPatch)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
c.StorageInstance.Store(downloadKey, data)

// Construct and return response
constructAndReturnResponse(w, r, downloadKey, paramMap)
}

func collectParams(params map[string][]string) map[string]string {
paramMap := make(map[string]string)
for key, values := range params {
if len(values) > 0 {
sanitizedValue := sanitizeInput(values[0])
paramMap[key] = sanitizedValue
}
}
return paramMap
}

func addTimestamp(paramMap map[string]string, path string) {
// if using patch and path is empty than add a timestamp with the value suffix
if path == "" {
allKeys := make([]string, 0, len(paramMap))
for k := range paramMap {
allKeys = append(allKeys, k)
}
// add a timestamp with the value suffix for all keys
timestamp := time.Now().UTC().Format(time.RFC3339)
for _, k := range allKeys {
paramMap[k+"_timestamp"] = timestamp
}
}

timestamp := time.Now().UTC().Format(time.RFC3339)
paramMap["timestamp"] = timestamp
}

func (c Config) handleDataStorage(downloadKey string, paramMap map[string]string, path string, isPatch bool) error {
var dataToStore map[string]interface{}

if isPatch {
existingData, err := c.StorageInstance.Retrieve(downloadKey)
if err != nil {
return err
}
mergeData(existingData, paramMap, strings.Split(path, "/"))
dataToStore = existingData
} else {
dataToStore = make(map[string]interface{})
for k, v := range paramMap {
dataToStore[k] = v
}
}

// add timestamp so that root level timestamp is always the latest of any updated value
timestamp := time.Now().UTC().Format(time.RFC3339)
dataToStore["timestamp"] = timestamp

return c.StorageInstance.Store(downloadKey, dataToStore)
}

func constructAndReturnResponse(w http.ResponseWriter, r *http.Request, downloadKey string, params map[string]string) {
urls := make(map[string]string)
for key := range params {
Expand All @@ -124,35 +69,3 @@ func constructAndReturnResponse(w http.ResponseWriter, r *http.Request, download
"parameter_urls": urls,
})
}

func mergeData(existingData map[string]interface{}, newData map[string]string, path []string) {
if len(path) == 0 || (len(path) == 1 && path[0] == "") {
for k, v := range newData {
existingData[k] = v
}
return
}

currentKey := path[0]
if _, exists := existingData[currentKey]; !exists {
existingData[currentKey] = make(map[string]interface{})
}

if nestedMap, ok := existingData[currentKey].(map[string]interface{}); ok {
mergeData(nestedMap, newData, path[1:])
} else {
existingData[currentKey] = newData
}
}

func ValidateUploadKey(uploadKey string) error {
uploadKey = strings.ToLower(uploadKey)
decoded, err := hex.DecodeString(uploadKey)
if err != nil {
return errors.New("uploadKey must be a 256 bit hex string")
}
if len(decoded) != 32 {
return errors.New("uploadKey must be a 256 bit hex string")
}
return nil
}

0 comments on commit aee017b

Please sign in to comment.