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] } }