From 33f732639f949c3099ee623f34d519b49ad21e9b Mon Sep 17 00:00:00 2001 From: Brandon Dyck Date: Fri, 16 Aug 2024 00:27:35 -0600 Subject: [PATCH] Create and open DB --- .gitignore | 1 + db.go | 98 ++++++++++++++++++++++++++++++++++++++++++++++++ db_test.go | 69 ++++++++++++++++++++++++++++++++++ go.mod | 25 ++++++++++++ go.sum | 30 +++++++++++++++ webadmin/main.go | 37 ++++++++++++++++++ 6 files changed, 260 insertions(+) create mode 100644 .gitignore create mode 100644 db.go create mode 100644 db_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 webadmin/main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722d5e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode diff --git a/db.go b/db.go new file mode 100644 index 0000000..433b554 --- /dev/null +++ b/db.go @@ -0,0 +1,98 @@ +package peachy + +import ( + "errors" + "fmt" + "os" + + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" +) + +const AppID = '🍑' + +var ErrInvalidDB = errors.New("invalid database file") +var ErrFileExists = errors.New("database file already exists") +var ErrFileNotExist = errors.New("database file does not exist") + +type DBError struct{ error } + +func (dbe DBError) Error() string { + return "database error: " + dbe.error.Error() +} + +type DB struct { + conn *sqlite.Conn +} + +func (db *DB) Close() { + db.conn.Close() +} + +func setupConn(conn *sqlite.Conn) error { + return nil +} + +func Open(path string) (db *DB, err error) { + var conn *sqlite.Conn + defer func() { + if err != nil { + conn.Close() + } + }() + + conn, err = sqlite.OpenConn(path, sqlite.OpenReadWrite|sqlite.OpenWAL) + switch sqlite.ErrCode(err) { + case sqlite.ResultOK: + case sqlite.ResultCantOpen: + return nil, ErrFileNotExist + case sqlite.ResultNotADB: + return nil, ErrInvalidDB + default: + return nil, DBError{err} + } + + var goodAppID bool + sqlitex.ExecuteTransient(conn, "PRAGMA application_id", &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + goodAppID = stmt.ColumnInt32(0) == AppID + return nil + }}) + if !goodAppID { + return nil, ErrInvalidDB + } + err = setupConn(conn) + if err != nil { + return nil, err + } + return &DB{conn: conn}, nil +} + +func Create(path string) (db *DB, err error) { + var conn *sqlite.Conn + defer func() { + if err != nil { + conn.Close() + } + }() + + finfo, _ := os.Stat(path) + if finfo != nil { + return nil, ErrFileExists + } + + conn, err = sqlite.OpenConn(path, sqlite.OpenCreate|sqlite.OpenReadWrite|sqlite.OpenWAL) + if err != nil { + return nil, fmt.Errorf("could not create database: %w", err) + } + query := fmt.Sprintf("PRAGMA application_id=%d", AppID) + err = sqlitex.ExecuteTransient(conn, query, nil) + if err != nil { + return nil, DBError{err} + } + err = setupConn(conn) + if err != nil { + return nil, err + } + return &DB{conn: conn}, nil +} diff --git a/db_test.go b/db_test.go new file mode 100644 index 0000000..3394545 --- /dev/null +++ b/db_test.go @@ -0,0 +1,69 @@ +package peachy_test + +import ( + "os" + "path/filepath" + "testing" + + "git.codemonkeysoftware.net/b/peachy-go" + "github.com/shoenig/test/must" + "zombiezen.com/go/sqlite" +) + +func pending(t *testing.T) { + t.Fatalf("test not implemented") +} + +func closeIfExists(db *peachy.DB) { + if db != nil { + db.Close() + } +} + +func TestOpen(t *testing.T) { + t.Run("Open succeeds on DB created by Create", func(t *testing.T) { + dir := t.TempDir() + dbPath := filepath.Join(dir, "test.db") + db, err := peachy.Create(dbPath) + must.NoError(t, err) + db.Close() + + db, err = peachy.Open(dbPath) + defer closeIfExists(db) + must.NotNil(t, db) + must.NoError(t, err) + + // TODO Make sure the DB works once it has any methods. + }) + t.Run("Open fails on alien SQLite DB", func(t *testing.T) { + dir := t.TempDir() + dbPath := filepath.Join(dir, "test.db") + conn, err := sqlite.OpenConn(dbPath) + must.NoError(t, err) + conn.Close() + + db, err := peachy.Open(dbPath) + defer closeIfExists(db) + must.Nil(t, db) + must.ErrorIs(t, err, peachy.ErrInvalidDB) + }) + t.Run("Open fails on nonexistent file", func(t *testing.T) { + dir := t.TempDir() + dbPath := filepath.Join(dir, "test.db") + db, err := peachy.Open(dbPath) + defer closeIfExists(db) + must.Nil(t, db) + must.ErrorIs(t, err, peachy.ErrFileNotExist) + must.FileNotExists(t, dbPath) + }) + t.Run("Open fails on non-SQLite file", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "test.db") + err := os.WriteFile(path, []byte("I'm not a database."), os.ModePerm) + must.NoError(t, err) + + db, err := peachy.Open(path) + defer closeIfExists(db) + must.Nil(t, db) + must.ErrorIs(t, err, peachy.ErrInvalidDB, must.Sprint(sqlite.ErrCode(err))) + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0cb3927 --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module git.codemonkeysoftware.net/b/peachy-go + +go 1.22.1 + +require ( + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c + github.com/shoenig/test v1.8.2 + zombiezen.com/go/sqlite v1.3.0 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/sys v0.16.0 // indirect + modernc.org/libc v1.41.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.7.2 // indirect + modernc.org/sqlite v1.29.1 // indirect +) + +replace github.com/shoenig/test v1.8.2 => ../shoenig-test diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c04df06 --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= +modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= +modernc.org/sqlite v1.29.1 h1:19GY2qvWB4VPw0HppFlZCPAbmxFU41r+qjKZQdQ1ryA= +modernc.org/sqlite v1.29.1/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0= +zombiezen.com/go/sqlite v1.3.0 h1:98g1gnCm+CNz6AuQHu0gqyw7gR2WU3O3PJufDOStpUs= +zombiezen.com/go/sqlite v1.3.0/go.mod h1:yRl27//s/9aXU3RWs8uFQwjkTG9gYNGEls6+6SvrclY= diff --git a/webadmin/main.go b/webadmin/main.go new file mode 100644 index 0000000..da69dcc --- /dev/null +++ b/webadmin/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "log" + "net" + "net/http" + + "github.com/pkg/browser" +) + +func handleHome(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "

Hello Peachy!

") +} + +func run() error { + listener, err := net.Listen("tcp", ":0") + if err != nil { + return err + } + port := listener.Addr().(*net.TCPAddr).Port + + homeURL := fmt.Sprintf("http://localhost:%d", port) + err = browser.OpenURL(homeURL) + if err != nil { + fmt.Printf("I couldn't open the browser automatically.\nDo it yourself, then go to %s.", homeURL) + } + + return http.Serve(listener, http.HandlerFunc(handleHome)) +} + +func main() { + err := run() + if err != nil { + log.Fatal(err) + } +}