├── .github └── workflows │ └── go.yml ├── LICENSE ├── Makefile ├── README.md ├── gdata.go ├── gdata.js ├── gdata_android.go ├── gdata_filesystem_impl.go ├── gdata_js.go ├── gdata_test.go ├── gdata_unix.go ├── gdata_windows.go ├── go.mod └── utils.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This file adheres to the YAML5 style. 2 | { 3 | "name": "Go", 4 | "on": ["push", "pull_request"], 5 | "jobs": { 6 | "build": { 7 | "name": "Build", 8 | "runs-on": "ubuntu-latest", 9 | "steps": [ 10 | { 11 | "name": "Set up Go 1.19", 12 | "uses": "actions/setup-go@v1", 13 | "with": {"go-version": 1.19}, 14 | "id": "go", 15 | }, 16 | {"name": "Check out code into the Go module directory", "uses": "actions/checkout@v1"}, 17 | {"name": "Linter", "run": "make ci-lint"}, 18 | {"name": "Test", "run": "make test"}, 19 | ], 20 | }, 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 quasilyte 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOPATH_DIR=`go env GOPATH` 2 | 3 | test: 4 | go test -v -count=3 . 5 | 6 | ci-lint: 7 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH_DIR)/bin v1.54.2 8 | $(GOPATH_DIR)/bin/golangci-lint run ./... 9 | @echo "everything is OK" 10 | 11 | lint: 12 | golangci-lint run ./... 13 | @echo "everything is OK" 14 | 15 | .PHONY: ci-lint lint test 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gdata 2 | 3 | ![Build Status](https://github.com/quasilyte/gdata/workflows/Go/badge.svg) 4 | [![PkgGoDev](https://pkg.go.dev/badge/mod/github.com/quasilyte/gdata)](https://pkg.go.dev/mod/github.com/quasilyte/gdata) 5 | 6 | A gamedata package that provides convenient cross-platform storage for games. 7 | 8 | Some examples of such gamedata that you might want to store: 9 | 10 | * Game settings 11 | * Save states 12 | * Replays 13 | * Pluging/mods metadata 14 | 15 | This package was made with [Ebitengine](https://github.com/hajimehoshi/ebiten/) in mind, but it should be usable with any kind of a game engine for Go. 16 | 17 | Platforms supported: 18 | 19 | * Windows (file system, AppData) 20 | * Linux (file system, ~/.local/share) 21 | * MacOS (file system, ~/.local/share) 22 | * Android (file system, app data directory) 23 | * Browser/wasm (local storage) 24 | 25 | This library tries to use the most conventional app data folder for every platform. 26 | 27 | It provides a simple key-value style API. It can be considered to be a platform-agnostic localStorage. 28 | 29 | This package was part of my game development framework which I used in all of my Go-powered games. Now I'm feel like it's ready to become a part of the ecosystem. 30 | 31 | ## Installation 32 | 33 | ```bash 34 | go get github.com/quasilyte/gdata 35 | ``` 36 | 37 | ## Quick Start 38 | 39 | ```go 40 | package main 41 | 42 | import ( 43 | "fmt" 44 | 45 | "github.com/quasilyte/gdata/v2" 46 | ) 47 | 48 | func main() { 49 | // m is a data manager; treat it as a connection to a filesystem. 50 | m, err := gdata.Open(gdata.Config{ 51 | AppName: "mygame", 52 | }) 53 | if err != nil { 54 | panic(err) 55 | } 56 | 57 | if err := m.SaveObjectProp("core", "save.data", []byte("mydata")); err != nil { 58 | panic(err) 59 | } 60 | if err := m.SaveObjectProp("core", "settings.json", []byte("settings")); err != nil { 61 | panic(err) 62 | } 63 | 64 | result, err := m.LoadObjectProp("core", "save.data") 65 | if err != nil { 66 | panic(err) 67 | } 68 | fmt.Println("=>", string(result)) // "mydata" 69 | 70 | fmt.Println(m.ObjectExists("core")) // true 71 | fmt.Println(m.ObjectPropExists("core", "save.data")) // true 72 | fmt.Println(m.ObjectPropExists("core", "settings.json")) // true 73 | 74 | { 75 | propKeys, err := m.ListObjectProps("core") 76 | if err != nil { 77 | panic(err) 78 | } 79 | for _, p := range propKeys { 80 | v, err := m.LoadObjectProp("core", p) 81 | if err != nil { 82 | panic(err) 83 | } 84 | fmt.Println("key=", p, "value=", string(v)) 85 | } 86 | } 87 | 88 | fmt.Println(m.ObjectPropExists("core", "save.data")) // true 89 | } 90 | ``` 91 | -------------------------------------------------------------------------------- /gdata.go: -------------------------------------------------------------------------------- 1 | package gdata 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Manager implements the main gamedata operations. 8 | // You can create a new manager by using Open function. 9 | type Manager struct { 10 | impl dataManagerImpl 11 | } 12 | 13 | // dataManagerImpl is an interface every platform-specific storage provider implements. 14 | type dataManagerImpl interface { 15 | ObjectPropPath(objectKey, propKey string) string 16 | 17 | ListObjectProps(objectKey string) ([]string, error) 18 | 19 | SaveObjectProp(objectKey, propKey string, data []byte) error 20 | 21 | LoadObjectProp(objectKey, propKey string) ([]byte, error) 22 | 23 | ObjectPropExists(objectKey, propKey string) bool 24 | ObjectExists(objectKey string) bool 25 | 26 | DeleteObjectProp(objectKey, propKey string) error 27 | DeleteObject(objectKey string) error 28 | } 29 | 30 | // Config affects the created gamedata manager behavior. 31 | type Config struct { 32 | // AppName is used as a part of the key used to store the game data. 33 | // 34 | // The exact effect depends on the platform, but generally it doesn't have 35 | // to reflect the application name perfectly. 36 | // 37 | // You need to use the same AppName to make sure that the game can 38 | // then load the previously saved data. 39 | // If you want to separate the data, use suffixes: "app" and "app2" data 40 | // will be stored completely independently. 41 | // 42 | // An empty app name is not allowed. 43 | AppName string 44 | } 45 | 46 | // Open attempts to create a gamedata manager. 47 | // 48 | // There are various cases when it can fail and you need to be 49 | // ready to handle that situation and run the game without the save states. 50 | // For instance, on wasm platforms it's using a localStorage which can be disabled. 51 | // In this case, a non-nil error will be returned and the game should continue 52 | // without any attempts to load or save data. 53 | // 54 | // One gamedata manager per game is enough. 55 | // You need to pass it explicitely as a part of your game's context. 56 | func Open(config Config) (*Manager, error) { 57 | if config.AppName == "" { 58 | return nil, errors.New("config.AppName can't be empty") 59 | } 60 | m, err := newDataManager(config) 61 | if err != nil { 62 | return nil, err 63 | } 64 | return &Manager{impl: m}, nil 65 | } 66 | 67 | // ListObjectProps returns all object property keys. 68 | // 69 | // If there is no such object, a nil slice is returned. 70 | // 71 | // The returned error is usually a file operation error. 72 | func (m *Manager) ListObjectProps(objectKey string) ([]string, error) { 73 | return m.impl.ListObjectProps(objectKey) 74 | } 75 | 76 | // SaveItem writes to the object's property with the associated key. 77 | // 78 | // Using an empty propKey is allowed. 79 | 80 | // Saving to an existing key overwrites it. 81 | // If object did not exist, it will be created automatically. 82 | // 83 | // The returned error is usually a file write error. 84 | func (m *Manager) SaveObjectProp(objectKey, propKey string, data []byte) error { 85 | return m.impl.SaveObjectProp(objectKey, fixPropKey(propKey), data) 86 | } 87 | 88 | // LoadObjectProp reads from the object's property using the provided key. 89 | // 90 | // Using an empty propKey is allowed. 91 | // 92 | // Loading a non-existing propKey is not an error, a nil slice will be returned. 93 | // The same rule applies to a non-existing object. 94 | // 95 | // If you want to know whether some object or a property exists, 96 | // use ObjectExists or ObjectPropExists method. 97 | // 98 | // The returned error is usually a file read error. 99 | func (m *Manager) LoadObjectProp(objectKey, propKey string) ([]byte, error) { 100 | return m.impl.LoadObjectProp(objectKey, fixPropKey(propKey)) 101 | } 102 | 103 | // ObjectExists reports whether the object was saved before. 104 | func (m *Manager) ObjectExists(objectKey string) bool { 105 | return m.impl.ObjectExists(objectKey) 106 | } 107 | 108 | // ObjectPropExists reports whether an object has the specified property. 109 | // 110 | // Using an empty propKey is allowed. 111 | // 112 | // If object itself doesn't exists, the function will report false. 113 | func (m *Manager) ObjectPropExists(objectKey, propKey string) bool { 114 | return m.impl.ObjectPropExists(objectKey, fixPropKey(propKey)) 115 | } 116 | 117 | // DeleteObject removes the object along with all of its properties (if any). 118 | // 119 | // Be careful with this function: it removes the data permanently. 120 | // There is no way to undo it. 121 | // 122 | // Trying to delete a non-existing object is not an error. 123 | // 124 | // The returned error is usually a file operation error. 125 | func (m *Manager) DeleteObject(objectKey string) error { 126 | return m.impl.DeleteObject(objectKey) 127 | } 128 | 129 | // DeleteObjectProp removes the object's property data. 130 | // 131 | // Using an empty propKey is allowed. 132 | // 133 | // Be careful with this function: it removes the data permanently. 134 | // There is no way to undo it. 135 | // 136 | // Trying to delete a non-existing propKey is not an error. 137 | // If object doesn't exist, it's not an error either. 138 | // 139 | // Deleting the last object's property does not delete 140 | // the object itself. Use DeleteObject if you want to 141 | // delete the object completely. 142 | // 143 | // The returned error is usually a file operation error. 144 | func (m *Manager) DeleteObjectProp(objectKey, propKey string) error { 145 | return m.impl.DeleteObjectProp(objectKey, fixPropKey(propKey)) 146 | } 147 | 148 | // ObjectPropPath returns a unique object property path. 149 | // 150 | // Using an empty propKey is allowed. 151 | // 152 | // On platforms with filesystem storage, it's an absolute file path. 153 | // On other platforms it's just some unique resource identifier. 154 | // 155 | // You can't treat it as a filesystem path unless you know what you're doing. 156 | // It's safe to use it for debugging and for things like map keys. 157 | // 158 | // Note that ObjectPropPath returns a potential data path, it doesn't care if 159 | // the item actually exists or not. 160 | // Use ObjectPropExists() method first if you need to know whether this 161 | // objectKey is used. 162 | func (m *Manager) ObjectPropPath(objectKey, propKey string) string { 163 | return m.impl.ObjectPropPath(objectKey, fixPropKey(propKey)) 164 | } 165 | 166 | func fixPropKey(propKey string) string { 167 | if propKey == "" { 168 | return "_objdat" 169 | } 170 | return propKey 171 | } 172 | -------------------------------------------------------------------------------- /gdata.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | const localStorage = window.localStorage; 3 | 4 | const propSeparator = ",,"; 5 | 6 | function mangle(propKey) { 7 | return "$" + propKey + "$"; 8 | } 9 | 10 | function unmangle(propKey) { 11 | return propKey.substring(1, propKey.length-1); 12 | } 13 | 14 | function objectPropPath(appKey, objectKey, mangledPropKey) { 15 | return appKey + "_" + objectKey + "__" + mangledPropKey; 16 | } 17 | 18 | function objectMetadataPath(appKey, objectKey) { 19 | return appKey + "_" + objectKey + "_proplist_"; 20 | } 21 | 22 | function listObjectProps(appKey, objectKey) { 23 | let metaKey = objectMetadataPath(appKey, objectKey); 24 | let v = localStorage.getItem(metaKey); 25 | if (v === null) { 26 | return null; 27 | } 28 | if (v === "") { 29 | return []; 30 | } 31 | return v.split(propSeparator).map(unmangle); 32 | } 33 | 34 | function saveObjectProp(appKey, objectKey, propKey, data) { 35 | let mangledPropKey = mangle(propKey); 36 | let metaKey = objectMetadataPath(appKey, objectKey); 37 | let v = localStorage.getItem(metaKey); 38 | if (v === null || v === '') { 39 | // Creating a fresh meta file with a single (new) prop key. 40 | localStorage.setItem(metaKey, mangledPropKey); 41 | } else { 42 | if (!v.includes(mangledPropKey)) { 43 | v += propSeparator + mangledPropKey; 44 | localStorage.setItem(metaKey, v); 45 | } 46 | } 47 | localStorage.setItem(objectPropPath(appKey, objectKey, mangledPropKey), data); 48 | } 49 | 50 | function loadObjectProp(appKey, objectKey, propKey) { 51 | let mangledPropKey = mangle(propKey); 52 | return localStorage.getItem(objectPropPath(appKey, objectKey, mangledPropKey)); 53 | } 54 | 55 | function objectPropExists(appKey, objectKey, propKey) { 56 | let mangledPropKey = mangle(propKey); 57 | return localStorage.getItem(objectPropPath(appKey, objectKey, mangledPropKey)) !== null; 58 | } 59 | 60 | function objectExists(appKey, objectKey) { 61 | return localStorage.getItem(objectMetadataPath(appKey, objectKey)) !== null; 62 | } 63 | 64 | function deleteObjectProp(appKey, objectKey, propKey) { 65 | let mangledPropKey = mangle(propKey); 66 | localStorage.removeItem(objectPropPath(appKey, objectKey, mangledPropKey)); 67 | 68 | let metaKey = objectMetadataPath(appKey, objectKey); 69 | let v = localStorage.getItem(metaKey); 70 | if (v !== null) { 71 | let parts = v.split(propSeparator); 72 | let index = parts.indexOf(mangledPropKey); 73 | if (index > -1) { 74 | parts.splice(index, 1); 75 | localStorage.setItem(metaKey, parts.join(propSeparator)); 76 | } 77 | } 78 | } 79 | 80 | function deleteObject(appKey, objectKey) { 81 | let metaKey = objectMetadataPath(appKey, objectKey); 82 | let v = localStorage.getItem(metaKey); 83 | if (v === null) { 84 | return; 85 | } 86 | 87 | let keys = v.split(propSeparator); 88 | for (let i = 0; i < keys.length; i++) { 89 | let mangledPropKey = keys[i]; 90 | localStorage.removeItem(objectPropPath(appKey, objectKey, mangledPropKey)); 91 | } 92 | localStorage.removeItem(metaKey); 93 | } 94 | 95 | return { 96 | "objectPropPath": objectPropPath, 97 | "listObjectProps": listObjectProps, 98 | "saveObjectProp": saveObjectProp, 99 | "loadObjectProp": loadObjectProp, 100 | "objectPropExists": objectPropExists, 101 | "objectExists": objectExists, 102 | "deleteObjectProp": deleteObjectProp, 103 | "deleteObject": deleteObject, 104 | }; 105 | }()); 106 | -------------------------------------------------------------------------------- /gdata_android.go: -------------------------------------------------------------------------------- 1 | //go:build android 2 | 3 | package gdata 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func newDataManager(config Config) (dataManagerImpl, error) { 12 | app, err := detectAndroidApp() 13 | if err != nil { 14 | return nil, err 15 | } 16 | dataPath := filepath.Join("/data/data/", app) 17 | if !fileExists(dataPath) { 18 | return nil, errors.New("can't find the app data directory") 19 | } 20 | m := &filesystemDataManager{ 21 | dataPath: dataPath, 22 | } 23 | return m, nil 24 | } 25 | 26 | func detectAndroidApp() (string, error) { 27 | data, err := os.ReadFile("/proc/self/cmdline") 28 | if err != nil { 29 | return "", err 30 | } 31 | // Trim any potential "\n" and remove the null bytes. 32 | copied := make([]byte, 0, len(data)) 33 | for _, ch := range data { 34 | switch ch { 35 | case 0, '\n': 36 | continue 37 | } 38 | copied = append(copied, ch) 39 | } 40 | result := string(copied) 41 | if result == "" { 42 | return "", errors.New("got empty output from /proc/self/cmdline") 43 | } 44 | return result, nil 45 | } 46 | -------------------------------------------------------------------------------- /gdata_filesystem_impl.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || linux || windows 2 | 3 | package gdata 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | // filesystemDataManager implements FS-based storage. 11 | // It works on Windows, MacOS, Linux, and Android. 12 | // 13 | // Objects are folders, files are their properties. 14 | type filesystemDataManager struct { 15 | dataPath string 16 | } 17 | 18 | func (m *filesystemDataManager) objectPath(objectKey string) string { 19 | return filepath.Join(m.dataPath, objectKey) 20 | } 21 | 22 | func (m *filesystemDataManager) ObjectPropPath(objectKey, propKey string) string { 23 | return filepath.Join(m.dataPath, objectKey, propKey) 24 | } 25 | 26 | func (m *filesystemDataManager) ListObjectProps(objectKey string) ([]string, error) { 27 | p := m.objectPath(objectKey) 28 | if !fileExists(p) { 29 | return nil, nil 30 | } 31 | files, err := os.ReadDir(p) 32 | if err != nil { 33 | return nil, err 34 | } 35 | result := make([]string, len(files)) 36 | for i, f := range files { 37 | result[i] = f.Name() 38 | } 39 | return result, nil 40 | } 41 | 42 | func (m *filesystemDataManager) SaveObjectProp(objectKey, propKey string, data []byte) error { 43 | p := m.objectPath(objectKey) 44 | if !fileExists(p) { 45 | if err := os.MkdirAll(p, os.ModePerm); err != nil { 46 | return err 47 | } 48 | } 49 | return os.WriteFile(filepath.Join(p, propKey), data, 0o666) 50 | } 51 | 52 | func (m *filesystemDataManager) LoadObjectProp(objectKey, propKey string) ([]byte, error) { 53 | p := m.ObjectPropPath(objectKey, propKey) 54 | if !fileExists(p) { 55 | return nil, nil 56 | } 57 | return os.ReadFile(p) 58 | } 59 | 60 | func (m *filesystemDataManager) ObjectPropExists(objectKey, propKey string) bool { 61 | return fileExists(m.ObjectPropPath(objectKey, propKey)) 62 | } 63 | 64 | func (m *filesystemDataManager) ObjectExists(objectKey string) bool { 65 | return fileExists(m.objectPath(objectKey)) 66 | } 67 | 68 | func (m *filesystemDataManager) DeleteObjectProp(objectKey, propKey string) error { 69 | p := m.ObjectPropPath(objectKey, propKey) 70 | if !fileExists(p) { 71 | return nil 72 | } 73 | return os.Remove(p) 74 | } 75 | 76 | func (m *filesystemDataManager) DeleteObject(objectKey string) error { 77 | p := m.objectPath(objectKey) 78 | // Since RemoveAll returns a nil error for a non-existing 79 | // path, we can avoid doing an explicit fileExists call. 80 | return os.RemoveAll(p) 81 | } 82 | -------------------------------------------------------------------------------- /gdata_js.go: -------------------------------------------------------------------------------- 1 | package gdata 2 | 3 | import ( 4 | "errors" 5 | "syscall/js" 6 | 7 | _ "embed" 8 | ) 9 | 10 | // Since we can't store objects inside the localStorage, 11 | // we store a metadata string per every object. 12 | // This metadata is a single string consisting of all 13 | // "filenames" (related property keys). 14 | 15 | //go:embed "gdata.js" 16 | var jsCode string 17 | 18 | type dataManager struct { 19 | appName string 20 | } 21 | 22 | func newDataManager(config Config) (dataManagerImpl, error) { 23 | const evalArgument = ` 24 | let ___result = true; 25 | try { 26 | const ___key = "storage__test__"; 27 | window.localStorage.setItem(___key, null); 28 | window.localStorage.removeItem(___key); 29 | } catch (e) { 30 | ___result = false; 31 | } 32 | ___result` 33 | hasLocalStorage := js.Global().Get("window").Call("eval", evalArgument).Bool() 34 | if !hasLocalStorage { 35 | return nil, errors.New("localStorage is not available") 36 | } 37 | m := &dataManager{ 38 | appName: config.AppName, 39 | } 40 | lib := js.Global().Get("window").Call("eval", jsCode) 41 | js.Global().Set("___gdata", lib) 42 | return m, nil 43 | } 44 | 45 | func (m *dataManager) getLib() js.Value { 46 | return js.Global().Get("___gdata") 47 | } 48 | 49 | func (m *dataManager) ObjectPropPath(objectKey, propKey string) string { 50 | return m.getLib().Call("objectPropPath", m.appName, objectKey, propKey).String() 51 | } 52 | 53 | func (m *dataManager) ListObjectProps(objectKey string) ([]string, error) { 54 | v := m.getLib().Call("listObjectProps", m.appName, objectKey) 55 | if v.IsNull() { 56 | return nil, nil 57 | } 58 | result := make([]string, v.Length()) 59 | for i := 0; i < v.Length(); i++ { 60 | result[i] = v.Index(i).String() 61 | } 62 | return result, nil 63 | } 64 | 65 | func (m *dataManager) SaveObjectProp(objectKey, propKey string, data []byte) error { 66 | m.getLib().Call("saveObjectProp", m.appName, objectKey, propKey, string(data)) 67 | return nil 68 | } 69 | 70 | func (m *dataManager) LoadObjectProp(objectKey, propKey string) ([]byte, error) { 71 | result := m.getLib().Call("loadObjectProp", m.appName, objectKey, propKey) 72 | if result.IsNull() { 73 | return nil, nil 74 | } 75 | return []byte(result.String()), nil 76 | } 77 | 78 | func (m *dataManager) ObjectPropExists(objectKey, propKey string) bool { 79 | return m.getLib().Call("objectPropExists", m.appName, objectKey, propKey).Bool() 80 | } 81 | 82 | func (m *dataManager) ObjectExists(objectKey string) bool { 83 | return m.getLib().Call("objectExists", m.appName, objectKey).Bool() 84 | } 85 | 86 | func (m *dataManager) DeleteObjectProp(objectKey, propKey string) error { 87 | m.getLib().Call("deleteObjectProp", m.appName, objectKey, propKey) 88 | return nil 89 | } 90 | 91 | func (m *dataManager) DeleteObject(objectKey string) error { 92 | m.getLib().Call("deleteObject", m.appName, objectKey) 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /gdata_test.go: -------------------------------------------------------------------------------- 1 | package gdata_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/quasilyte/gdata/v2" 8 | ) 9 | 10 | func TestSaveLoad(t *testing.T) { 11 | m, err := gdata.Open(gdata.Config{ 12 | AppName: "gdata_test", 13 | }) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | const ( 19 | testObjectKey = "obj" 20 | testItemKey = "testitem.txt" 21 | ) 22 | 23 | for round := 0; round < 3; round++ { 24 | for i := 0; i < 2; i++ { 25 | // Deleting a potentially non-existing item. 26 | // For a second run it acts like a state reset function. 27 | if err := m.DeleteObjectProp(testObjectKey, testItemKey); err != nil { 28 | t.Fatalf("delete should never result in an error here (got %v)", err) 29 | } 30 | } 31 | for i := 0; i < 2; i++ { 32 | if err := m.DeleteObject(testObjectKey); err != nil { 33 | t.Fatalf("delete should never result in an error here (got %v)", err) 34 | } 35 | } 36 | 37 | if m.ObjectPropExists(testObjectKey, testItemKey) { 38 | t.Fatalf("%s.%s item should not exist yet", testObjectKey, testItemKey) 39 | } 40 | if m.ObjectExists(testObjectKey) { 41 | t.Fatalf("%s item should not exist yet", testObjectKey) 42 | } 43 | 44 | data := []byte("example data") 45 | if err := m.SaveObjectProp(testObjectKey, testItemKey, data); err != nil { 46 | t.Fatalf("error saving %s data", testItemKey) 47 | } 48 | 49 | if !m.ObjectPropExists(testObjectKey, testItemKey) { 50 | t.Fatalf("%s.%s item should exist after a successful save operation", testObjectKey, testItemKey) 51 | } 52 | if !m.ObjectExists(testObjectKey) { 53 | t.Fatalf("%s item should exist after a successful save operation", testObjectKey) 54 | } 55 | 56 | loadedData, err := m.LoadObjectProp(testObjectKey, testItemKey) 57 | if err != nil { 58 | t.Fatalf("loading %s error: %v", testItemKey, err) 59 | } 60 | 61 | if !bytes.Equal(data, loadedData) { 62 | t.Fatalf("saved and loaded data mismatch:\nwant: %q\n have: %q", string(data), string(loadedData)) 63 | } 64 | 65 | // Now we're deleting an existing item. 66 | if err := m.DeleteObjectProp(testObjectKey, testItemKey); err != nil { 67 | t.Fatalf("delete should never result in an error here (got %v)", err) 68 | } 69 | 70 | if m.ObjectPropExists(testObjectKey, testItemKey) { 71 | t.Fatalf("%s item should not exist after a successful delete operation", testItemKey) 72 | } 73 | if !m.ObjectExists(testObjectKey) { 74 | t.Fatalf("%s object should remain existing after removing its property", testObjectKey) 75 | } 76 | 77 | if err := m.DeleteObject(testObjectKey); err != nil { 78 | t.Fatalf("error deleting %s object", testObjectKey) 79 | } 80 | if m.ObjectExists(testObjectKey) { 81 | t.Fatalf("%s object should not exist after being deleted", testObjectKey) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /gdata_unix.go: -------------------------------------------------------------------------------- 1 | //go:build (darwin || linux) && !android 2 | 3 | package gdata 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | func newDataManager(config Config) (dataManagerImpl, error) { 11 | home, err := os.UserHomeDir() 12 | if err != nil { 13 | return nil, err 14 | } 15 | dataPath := filepath.Join(home, ".local", "share", config.AppName) 16 | if err := mkdirAll(dataPath); err != nil { 17 | return nil, err 18 | } 19 | m := &filesystemDataManager{ 20 | dataPath: dataPath, 21 | } 22 | return m, nil 23 | } 24 | -------------------------------------------------------------------------------- /gdata_windows.go: -------------------------------------------------------------------------------- 1 | package gdata 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | func newDataManager(config Config) (dataManagerImpl, error) { 10 | appData := os.Getenv("AppData") 11 | if appData == "" { 12 | return nil, errors.New("AppData env var is undefined") 13 | } 14 | dataPath := filepath.Join(appData, config.AppName) 15 | if err := mkdirAll(dataPath); err != nil { 16 | return nil, err 17 | } 18 | m := &filesystemDataManager{ 19 | dataPath: dataPath, 20 | } 21 | return m, nil 22 | } 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/quasilyte/gdata/v2 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package gdata 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func fileExists(path string) bool { 8 | _, err := os.Stat(path) 9 | return !os.IsNotExist(err) 10 | } 11 | 12 | func mkdirAll(path string) error { 13 | if fileExists(path) { 14 | return nil 15 | } 16 | return os.MkdirAll(path, 0755) 17 | } 18 | --------------------------------------------------------------------------------