Signed-off-by: David Anderson <danderson@tailscale.com>main
parent
755396d6fe
commit
96afd1db46
@ -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) |
||||
} |
||||
@ -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 |
||||
} |
||||
Loading…
Reference in new issue