summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Anderson <danderson@tailscale.com>2022-07-12 11:45:04 -0700
committerDave Anderson <dave@natulte.net>2022-07-12 11:56:38 -0700
commit96afd1db467d44a7a72328f3e6aecfc0b0ff46bb (patch)
tree0360aa9c0c7c7fe9bded4ac2d506450e489f8fee
parent755396d6fe77cbee6a32937fea9e58d5c175d047 (diff)
downloadtailscale-96afd1db467d44a7a72328f3e6aecfc0b0ff46bb.tar.xz
tailscale-96afd1db467d44a7a72328f3e6aecfc0b0ff46bb.zip
jsondb: small package to load/save JSON DBs.
Signed-off-by: David Anderson <danderson@tailscale.com>
-rw-r--r--jsondb/db.go58
-rw-r--r--jsondb/db_test.go56
2 files changed, 114 insertions, 0 deletions
diff --git a/jsondb/db.go b/jsondb/db.go
new file mode 100644
index 000000000..3a0d8060a
--- /dev/null
+++ b/jsondb/db.go
@@ -0,0 +1,58 @@
+// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package jsondb provides a trivial "database": a Go object saved to
+// disk as JSON.
+package jsondb
+
+import (
+ "encoding/json"
+ "errors"
+ "io/fs"
+ "os"
+
+ "tailscale.com/atomicfile"
+)
+
+// DB is a database backed by a JSON file.
+type DB[T any] struct {
+ // Data is the contents of the database.
+ Data *T
+
+ path string
+}
+
+// Open opens the database at path, creating it with a zero value if
+// necessary.
+func Open[T any](path string) (*DB[T], error) {
+ bs, err := os.ReadFile(path)
+ if errors.Is(err, fs.ErrNotExist) {
+ return &DB[T]{
+ Data: new(T),
+ path: path,
+ }, nil
+ } else if err != nil {
+ return nil, err
+ }
+
+ var val T
+ if err := json.Unmarshal(bs, &val); err != nil {
+ return nil, err
+ }
+
+ return &DB[T]{
+ Data: &val,
+ path: path,
+ }, nil
+}
+
+// Save writes db.Data back to disk.
+func (db *DB[T]) Save() error {
+ bs, err := json.Marshal(db.Data)
+ if err != nil {
+ return err
+ }
+
+ return atomicfile.WriteFile(db.path, bs, 0600)
+}
diff --git a/jsondb/db_test.go b/jsondb/db_test.go
new file mode 100644
index 000000000..df784194d
--- /dev/null
+++ b/jsondb/db_test.go
@@ -0,0 +1,56 @@
+// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package jsondb
+
+import (
+ "log"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+func TestDB(t *testing.T) {
+ dir, err := os.MkdirTemp("", "db-test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(dir)
+
+ path := filepath.Join(dir, "db.json")
+ db, err := Open[testDB](path)
+ if err != nil {
+ t.Fatalf("creating empty DB: %v", err)
+ }
+
+ if diff := cmp.Diff(db.Data, &testDB{}, cmp.AllowUnexported(testDB{})); diff != "" {
+ t.Fatalf("unexpected empty DB content (-got+want):\n%s", diff)
+ }
+ db.Data.MyString = "test"
+ db.Data.unexported = "don't keep"
+ db.Data.AnInt = 42
+ if err := db.Save(); err != nil {
+ t.Fatalf("saving database: %v", err)
+ }
+
+ db2, err := Open[testDB](path)
+ if err != nil {
+ log.Fatalf("opening DB again: %v", err)
+ }
+ want := &testDB{
+ MyString: "test",
+ AnInt: 42,
+ }
+ if diff := cmp.Diff(db2.Data, want, cmp.AllowUnexported(testDB{})); diff != "" {
+ t.Fatalf("unexpected saved DB content (-got+want):\n%s", diff)
+ }
+}
+
+type testDB struct {
+ MyString string
+ unexported string
+ AnInt int64
+}