From 96afd1db467d44a7a72328f3e6aecfc0b0ff46bb Mon Sep 17 00:00:00 2001 From: David Anderson Date: Tue, 12 Jul 2022 11:45:04 -0700 Subject: [PATCH] jsondb: small package to load/save JSON DBs. Signed-off-by: David Anderson --- jsondb/db.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++ jsondb/db_test.go | 56 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 jsondb/db.go create mode 100644 jsondb/db_test.go 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 +}