jsondb/jsondb.go

460 lines
9.8 KiB
Go

package jsondb
import (
"encoding/json"
"errors"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"time"
"git.0x0001f346.de/andreas/utils"
)
type Cluster map[string]Database
type Database map[string]interface{}
var cluster Cluster = Cluster{}
var locationOfCluster string = utils.GetPathToParrentFolderOfThisExecutable()
var nameOfCluster string = "jsondb"
var databaseIsLocked map[string]bool = map[string]bool{}
const databaseExtension string = ".json"
// CreateDatabase creates a database or returns nil if it already exists
func CreateDatabase(nameOfDatabase string) error {
_, databaseAlreadyExists := cluster[nameOfDatabase]
if databaseAlreadyExists {
return nil
}
err := safeDatabase(nameOfDatabase, Database{})
if err != nil {
return err
}
unlockDatabase(nameOfDatabase)
return nil
}
// DeleteDatabase deletes a database
func DeleteDatabase(nameOfDatabase string) error {
_, databaseExists := cluster[nameOfDatabase]
if !databaseExists {
return nil
}
lockDatabase(nameOfDatabase)
err := deleteDatabase(nameOfDatabase)
if err != nil {
return err
}
return nil
}
// DeleteKeyFromDatabase deletes a key in a database and write it to file
func DeleteKeyFromDatabase(nameOfDatabase string, key string) error {
_, databaseExists := cluster[nameOfDatabase]
if !databaseExists {
return errors.New("database does not exist")
}
lockDatabase(nameOfDatabase)
err := ReloadDatabase(nameOfDatabase)
if err != nil {
unlockDatabase(nameOfDatabase)
return err
}
database, err := getDeepCopyOfDatabase(nameOfDatabase)
if err != nil {
unlockDatabase(nameOfDatabase)
return err
}
delete(database, key)
err = safeDatabase(nameOfDatabase, database)
if err != nil {
unlockDatabase(nameOfDatabase)
return err
}
cluster[nameOfDatabase] = database
unlockDatabase(nameOfDatabase)
return nil
}
// GetDatabase returns a database if it exists
func GetDatabase(nameOfDatabase string) (Database, error) {
database, err := getDeepCopyOfDatabase(nameOfDatabase)
if err != nil {
return nil, err
}
return database, nil
}
// GetKeyFromDatabase returns a key from a database if both exist
func GetKeyFromDatabase(nameOfDatabase string, key string) (interface{}, error) {
database, databaseExists := cluster[nameOfDatabase]
if !databaseExists {
return nil, errors.New("database does not exist")
}
value, keyExists := database[key]
if !keyExists {
return nil, errors.New("key does not exist")
}
return value, nil
}
// GetLocationOfCluster returns locationOfCluster
func GetLocationOfCluster() string {
return locationOfCluster
}
// GetNameOfCluster returns nameOfCluster
func GetNameOfCluster() string {
return nameOfCluster
}
// LoadCluster loads all databases from the current cluster
func LoadCluster() error {
if !utils.DoesFolderExist(getFullPathToCluster()) {
err := createCluster()
if err != nil {
return err
}
}
for _, p := range utils.GetAllFilesInFolderByExtension(getFullPathToCluster(), databaseExtension) {
databaseName := strings.TrimSuffix(path.Base(p), filepath.Ext(path.Base(p)))
if !isValidName(databaseName) {
continue
}
database, err := loadDatabase(databaseName)
if err != nil {
return err
}
cluster[databaseName] = database
databaseIsLocked[databaseName] = false
}
return nil
}
// ReloadDatabase reloads a database from file
func ReloadDatabase(nameOfDatabase string) error {
_, databaseExists := cluster[nameOfDatabase]
if !databaseExists {
return errors.New("database does not exist")
}
database, err := loadDatabase(nameOfDatabase)
if err != nil {
return err
}
cluster[nameOfDatabase] = database
return nil
}
// RenameDatabase renames a database
func RenameDatabase(nameOfDatabase string, newNameOfDatabase string) error {
_, databaseExists := cluster[nameOfDatabase]
if !databaseExists {
return errors.New("database does not exist")
}
lockDatabase(nameOfDatabase)
err := ReloadDatabase(nameOfDatabase)
if err != nil {
unlockDatabase(nameOfDatabase)
return err
}
database, err := getDeepCopyOfDatabase(nameOfDatabase)
if err != nil {
unlockDatabase(nameOfDatabase)
return err
}
err = safeDatabase(newNameOfDatabase, database)
if err != nil {
unlockDatabase(nameOfDatabase)
return err
}
cluster[newNameOfDatabase] = database
lockDatabase(newNameOfDatabase)
err = deleteDatabase(nameOfDatabase)
if err != nil {
unlockDatabase(nameOfDatabase)
unlockDatabase(newNameOfDatabase)
return err
}
unlockDatabase(newNameOfDatabase)
return nil
}
// RenameKeyInDatabase renames a key in a database and write it to file
func RenameKeyInDatabase(nameOfDatabase string, keyOld string, keyNew string) error {
_, databaseExists := cluster[nameOfDatabase]
if !databaseExists {
return errors.New("database does not exist")
}
lockDatabase(nameOfDatabase)
err := ReloadDatabase(nameOfDatabase)
if err != nil {
unlockDatabase(nameOfDatabase)
return err
}
database, err := getDeepCopyOfDatabase(nameOfDatabase)
if err != nil {
unlockDatabase(nameOfDatabase)
return err
}
database[keyNew] = database[keyOld]
delete(database, keyOld)
err = safeDatabase(nameOfDatabase, database)
if err != nil {
unlockDatabase(nameOfDatabase)
return err
}
cluster[nameOfDatabase] = database
unlockDatabase(nameOfDatabase)
return nil
}
// SetDatabase sets a database and writes it to file
func SetDatabase(nameOfDatabase string, database Database) error {
_, databaseExists := cluster[nameOfDatabase]
if !databaseExists {
return errors.New("database does not exist")
}
lockDatabase(nameOfDatabase)
err := safeDatabase(nameOfDatabase, database)
if err != nil {
unlockDatabase(nameOfDatabase)
return err
}
cluster[nameOfDatabase] = database
unlockDatabase(nameOfDatabase)
return nil
}
// SetKeyInDatabase sets a key from in a database and write it to file
func SetKeyInDatabase(nameOfDatabase string, key string, value interface{}) error {
_, databaseExists := cluster[nameOfDatabase]
if !databaseExists {
return errors.New("database does not exist")
}
lockDatabase(nameOfDatabase)
err := ReloadDatabase(nameOfDatabase)
if err != nil {
unlockDatabase(nameOfDatabase)
return err
}
database, err := getDeepCopyOfDatabase(nameOfDatabase)
if err != nil {
unlockDatabase(nameOfDatabase)
return err
}
database[key] = value
err = safeDatabase(nameOfDatabase, database)
if err != nil {
unlockDatabase(nameOfDatabase)
return err
}
cluster[nameOfDatabase] = database
unlockDatabase(nameOfDatabase)
return nil
}
// SetLocationOfCluster sets locationOfCluster if p is a valid folder
func SetLocationOfCluster(location string) error {
if !utils.DoesFolderExist(location) {
return errors.New("not a valid cluster location")
}
locationOfCluster = location
return nil
}
// SetNameOfCluster sets nameOfCluster if s is a valid cluster name
func SetNameOfCluster(name string) error {
if !isValidName(name) {
return errors.New("not a valid cluster name")
}
nameOfCluster = name
return nil
}
func createCluster() error {
if utils.DoesFolderExist(getFullPathToCluster()) {
return nil
}
err := utils.CreateFolder(getFullPathToCluster())
if err != nil {
return err
}
return nil
}
func deleteDatabase(nameOfDatabase string) error {
_, databaseExists := cluster[nameOfDatabase]
if !databaseExists {
return nil
}
err := os.Remove(getFullPathToDatabase(nameOfDatabase))
if err != nil {
return err
}
delete(cluster, nameOfDatabase)
delete(databaseIsLocked, nameOfDatabase)
return nil
}
func getDeepCopyOfDatabase(nameOfDatabase string) (Database, error) {
originalDatabase, databaseExists := cluster[nameOfDatabase]
if !databaseExists {
return nil, errors.New("database does not exist")
}
copyOfDatabase := make(Database)
for key, value := range originalDatabase {
copyOfDatabase[key] = value
}
return copyOfDatabase, nil
}
func getFullPathToCluster() string {
return filepath.Join(
locationOfCluster,
nameOfCluster,
)
}
func getFullPathToDatabase(nameOfDatabase string) string {
return filepath.Join(
getFullPathToCluster(),
fmt.Sprintf("%s%s", nameOfDatabase, databaseExtension),
)
}
func isValidName(s string) bool {
if len(s) == 0 {
return false
}
whitelist := utils.BuildWhitelistForStringSanitisation(
[]string{
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
"n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
},
)
return !utils.DoesStringContainsNonWhitelistedSubstrings(s, whitelist)
}
func loadDatabase(nameOfDatabase string) (Database, error) {
dbContentAsString, err := utils.LoadStringFromFile(
getFullPathToDatabase(nameOfDatabase),
)
if err != nil {
return Database{}, err
}
var database Database
err = json.Unmarshal([]byte(dbContentAsString), &database)
if err != nil {
return Database{}, err
}
return database, nil
}
func lockDatabase(nameOfDatabase string) {
waitForDatabaseToUnlock(nameOfDatabase)
databaseIsLocked[nameOfDatabase] = true
}
func safeDatabase(nameOfDatabase string, database Database) error {
databaseAsJSONBytes, err := json.MarshalIndent(database, "", "\t")
if err != nil {
return errors.New("could not marshal database: " + nameOfDatabase)
}
err = utils.WriteStringToFile(getFullPathToDatabase(nameOfDatabase), string(databaseAsJSONBytes))
if err != nil {
return errors.New("could not write to file: " + getFullPathToDatabase(nameOfDatabase))
}
return nil
}
func unlockDatabase(nameOfDatabase string) {
databaseIsLocked[nameOfDatabase] = false
}
func waitForDatabaseToUnlock(nameOfDatabase string) {
isLocked, databaseExists := databaseIsLocked[nameOfDatabase]
if !databaseExists {
return
}
for {
if !isLocked {
break
}
time.Sleep(5 * time.Millisecond)
isLocked = databaseIsLocked[nameOfDatabase]
}
}