detached be2120d

Refactor database initialization and backup logic

commit
be2120d9a4ed89dbc568f286723ac4865b526971
author
bnema · · 2023-09-15 15:10 UTC
parent
ae815f0
signature
unsigned
11 files · +804 -173
M .gitignore +1 -1
@@ -1,7 +1,7 @@
11 .env
22 node_modules/
3+*tmp/
34 tmp/*
45 gordon.yml
56 bun.lockb
67 *.log
7-
@@ -1,7 +1,7 @@
1 .env1 .env
2 node_modules/2 node_modules/
3+*tmp/
3 tmp/*4 tmp/*
4 gordon.yml5 gordon.yml
5 bun.lockb6 bun.lockb
6 *.log7 *.log
7-
D :memory +0 -0 binary file: no text diff

binary file: no text diff

A changes.txt +326 -0
@@ -0,0 +1,326 @@
1+diff --git a/:memory b/:memory
2+new file mode 100644
3+index 0000000..9760229
4+Binary files /dev/null and b/:memory differ
5+diff --git a/cmd/main.go b/cmd/main.go
6+index 6a95d32..b8e2cd4 100644
7+--- a/cmd/main.go
8++++ b/cmd/main.go
9+@@ -4,17 +4,30 @@ import (
10+ "fmt"
11+ "log/slog"
12+
13++ "github.com/labstack/echo/v4"
14++
15+ "github.com/bnema/gordon/internal/app"
16+ "github.com/bnema/gordon/internal/httpserve"
17+- "github.com/labstack/echo/v4"
18+ )
19+
20+ func main() {
21+ a := app.NewApp()
22++
23++ // Initialize database
24++ db, err := app.InitializeDB(a)
25++ if err != nil {
26++ slog.Error("Failed to load database", err)
27++ } else {
28++ slog.Info("Database loaded")
29++ }
30++
31++ fmt.Println(db)
32++
33+ e := echo.New()
34+ e.HideBanner = true
35+ e.HidePort = true
36+ e = httpserve.RegisterRoutes(e, a)
37+ slog.Info("Starting server", "port", a.HttpPort)
38+ slog.Error("Server error", e.Start(fmt.Sprintf(":%d", a.HttpPort)))
39++
40+ }
41+diff --git a/go.mod b/go.mod
42+index 35e2d3c..8a3ac59 100644
43+--- a/go.mod
44++++ b/go.mod
45+@@ -24,6 +24,7 @@ require (
46+ github.com/gorilla/securecookie v1.1.1 // indirect
47+ github.com/gorilla/sessions v1.2.1 // indirect
48+ github.com/klauspost/compress v1.16.7 // indirect
49++ github.com/kr/text v0.1.0 // indirect
50+ github.com/labstack/gommon v0.4.0 // indirect
51+ github.com/mattn/go-colorable v0.1.13 // indirect
52+ github.com/mattn/go-isatty v0.0.19 // indirect
53+@@ -35,6 +36,7 @@ require (
54+ github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
55+ github.com/opencontainers/runc v1.1.9 // indirect
56+ github.com/pkg/errors v0.9.1 // indirect
57++ github.com/rogpeppe/go-internal v1.11.0 // indirect
58+ github.com/sirupsen/logrus v1.9.3 // indirect
59+ github.com/valyala/bytebufferpool v1.0.0 // indirect
60+ github.com/valyala/fasttemplate v1.2.2 // indirect
61+@@ -45,5 +47,6 @@ require (
62+ golang.org/x/text v0.11.0 // indirect
63+ golang.org/x/time v0.3.0 // indirect
64+ golang.org/x/tools v0.7.0 // indirect
65++ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
66+ gotest.tools/v3 v3.5.0 // indirect
67+ )
68+diff --git a/go.sum b/go.sum
69+index 7b9b857..17b0bcd 100644
70+--- a/go.sum
71++++ b/go.sum
72+@@ -39,6 +39,11 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
73+ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
74+ github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
75+ github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
76++github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
77++github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
78++github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
79++github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
80++github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
81+ github.com/labstack/echo-contrib v0.15.0 h1:9K+oRU265y4Mu9zpRDv3X+DGTqUALY6oRHCSZZKCRVU=
82+ github.com/labstack/echo-contrib v0.15.0/go.mod h1:lei+qt5CLB4oa7VHTE0yEfQSEB9XTJI1LUqko9UWvo4=
83+ github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
84+@@ -72,6 +77,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
85+ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
86+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
87+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
88++github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
89++github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
90+ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
91+ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
92+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
93+@@ -153,8 +160,9 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
94+ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
95+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
96+ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
97+-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
98+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
99++gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
100++gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
101+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
102+ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
103+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
104+diff --git a/internal/app/app.go b/internal/app/app.go
105+index c6f49ac..1dd44b8 100644
106+--- a/internal/app/app.go
107++++ b/internal/app/app.go
108+@@ -27,6 +27,7 @@ const (
109+ type App struct {
110+ TemplateFS fs.FS
111+ PublicFS fs.FS
112++ DB interface{}
113+ BuildVersion string
114+ BuildDir string
115+ DBDir string
116+diff --git a/internal/app/sqlite.go b/internal/app/sqlite.go
117+index 9f1b1ab..ddc6a1e 100644
118+--- a/internal/app/sqlite.go
119++++ b/internal/app/sqlite.go
120+@@ -5,58 +5,78 @@ import (
121+ "database/sql"
122+ "fmt"
123+ "os"
124++ "path/filepath"
125+
126+- sqlite3 "github.com/mattn/go-sqlite3"
127++ "github.com/bnema/gordon/internal/database/migrate"
128++ "github.com/mattn/go-sqlite3"
129+ )
130+
131+-// Initialize in-memory SQLite database and returns its connection
132+-func InitDB() (db *sql.DB, err error) {
133+- db, err = sql.Open("sqlite3", "file::memory?cache=shared")
134+- if err != nil {
135+- return nil, fmt.Errorf("failed to open in-memory database: %v", err)
136+- }
137+- return
138+-}
139+-
140+-// CloseDB closes the database connection and backs up the in-memory database to disk
141+-func CloseDB(db *sql.DB, a *App) error {
142+- if err := BackupDB(db, a); err != nil {
143+- return err
144+- }
145++// InitializeDB initializes the SQLite database. If a database file exists on disk, it loads it into memory.
146++// Otherwise, it creates a new in-memory database and bootstraps it.
147++func InitializeDB(a *App) (*sql.DB, error) {
148++ dbfp := a.GetDBFilePath()
149+
150+- if err := db.Close(); err != nil {
151+- return fmt.Errorf("failed to close database: %v", err)
152++ // Check if the directory exists, if not create it
153++ dir := filepath.Dir(a.DBDir)
154++ fmt.Println("dir", dir)
155++ if _, err := os.Stat(dir); os.IsNotExist(err) {
156++ err = os.MkdirAll(dir, 0755)
157++ if err != nil {
158++ return nil, fmt.Errorf("failed to create directory: %v", err)
159++ }
160+ }
161+
162+- return nil
163+-}
164++ // If the database file doesn't exist on disk
165++ if _, err := os.Stat(dbfp); os.IsNotExist(err) {
166++ memDB, err := initInMemoryDB()
167++ if err != nil {
168++ return nil, err
169++ }
170+
171+-// LoadDBIntoMemory loads the database from disk into memory
172+-func LoadDBIntoMemory(a *App) (*sql.DB, error) {
173+- dbfp := a.GetDBFilePath()
174++ // Run the bootstrap
175++ RunBootstrap(memDB)
176+
177+- if _, err := os.Stat(dbfp); err != nil {
178+- return nil, nil
179++ // Explicitly backup the in-memory database to disk
180++ err = BackupDB(memDB, a)
181++ if err != nil {
182++ return nil, fmt.Errorf("failed to backup in-memory database to disk: %v", err)
183++ }
184+ }
185+
186+- memDB, err := InitDB()
187++ memDB, err := initInMemoryDB()
188+ if err != nil {
189+ return nil, err
190+ }
191+-
192+ diskDB, err := sql.Open("sqlite3", dbfp)
193+ if err != nil {
194+ return nil, fmt.Errorf("failed to open database file: %v", err)
195+ }
196++ defer diskDB.Close()
197+
198+ if err := transferData(diskDB, memDB); err != nil {
199+ return nil, err
200+ }
201+-
202+ return memDB, nil
203+ }
204+
205+-// transferData transfers data from srcDB to dstDB
206++func initInMemoryDB() (*sql.DB, error) {
207++ db, err := sql.Open("sqlite3", "file::memory?cache=shared")
208++ if err != nil {
209++ return nil, fmt.Errorf("failed to open in-memory database: %v", err)
210++ }
211++ return db, nil
212++}
213++
214++func CloseAndBackupDB(db *sql.DB, a *App) error {
215++ if err := BackupDB(db, a); err != nil {
216++ return err
217++ }
218++ if err := db.Close(); err != nil {
219++ return fmt.Errorf("failed to close database: %v", err)
220++ }
221++ return nil
222++}
223++
224+ func transferData(srcDB, dstDB *sql.DB) error {
225+ srcConn, err := srcDB.Conn(context.Background())
226+ if err != nil {
227+@@ -74,35 +94,50 @@ func transferData(srcDB, dstDB *sql.DB) error {
228+ return dstConn.Raw(func(dstRawConn interface{}) error {
229+ srcSqlite3Conn := srcRawConn.(*sqlite3.SQLiteConn)
230+ dstSqlite3Conn := dstRawConn.(*sqlite3.SQLiteConn)
231+-
232+ _, err := srcSqlite3Conn.Backup("main", dstSqlite3Conn, "main")
233+ return err
234+ })
235+ })
236+ }
237+
238+-// BackupDB backs up the in-memory database to disk
239+ func BackupDB(db *sql.DB, a *App) error {
240+ dbfp := a.GetDBFilePath()
241+-
242+ conn, err := db.Conn(context.Background())
243+ if err != nil {
244+ return fmt.Errorf("failed to get underlying database connection: %v", err)
245+ }
246++ defer conn.Close()
247+
248+ return conn.Raw(func(sqliteConn interface{}) error {
249+ sqlite3Conn := sqliteConn.(*sqlite3.SQLiteConn)
250+- backupDB, err := sqlite3Conn.Backup(dbfp, sqlite3Conn, "main")
251++ diskDB, err := sql.Open("sqlite3", dbfp)
252++ fmt.Println("diskDB", diskDB)
253++ if err != nil {
254++ return fmt.Errorf("failed to open destination database file: %v", err)
255++ }
256++ defer diskDB.Close()
257++
258++ diskConn, err := diskDB.Conn(context.Background())
259+ if err != nil {
260+- return fmt.Errorf("failed to create backup: %v", err)
261++ return fmt.Errorf("failed to get connection from diskDB: %v", err)
262+ }
263++ defer diskConn.Close()
264++
265++ var backupDB *sqlite3.SQLiteBackup
266++ err = diskConn.Raw(func(diskRawConn interface{}) error {
267++ backupDB, err = sqlite3Conn.Backup("main", diskRawConn.(*sqlite3.SQLiteConn), "main")
268++ return err
269++ })
270+ defer backupDB.Close()
271+
272+ _, err = backupDB.Step(-1)
273+ if err != nil {
274+ return fmt.Errorf("failed to backup in-memory database: %v", err)
275+ }
276+-
277+ return nil
278+ })
279+ }
280++
281++func RunBootstrap(db *sql.DB) error {
282++ return migrate.CreateUserTable(db)
283++}
284+diff --git a/internal/database/migrate/bootstrapdb.go b/internal/database/migrate/bootstrapdb.go
285+new file mode 100644
286+index 0000000..1f53246
287+--- /dev/null
288++++ b/internal/database/migrate/bootstrapdb.go
289+@@ -0,0 +1,24 @@
290++package migrate
291++
292++import (
293++ "database/sql"
294++ "fmt"
295++)
296++
297++// createUserTable creates the 'user' table in the provided SQLite database.
298++func CreateUserTable(db *sql.DB) error {
299++ createTableSQL := `
300++ CREATE TABLE user (
301++ id INTEGER PRIMARY KEY AUTOINCREMENT,
302++ username TEXT NOT NULL,
303++ password TEXT NOT NULL, -- This should be hashed and salted in a real-world scenario.
304++ oauth_token TEXT
305++ );`
306++
307++ _, err := db.Exec(createTableSQL)
308++ if err != nil {
309++ return fmt.Errorf("failed to create user table: %v", err)
310++ }
311++
312++ return nil
313++}
314+diff --git a/internal/httpserve/handler/adminroutes.go b/internal/httpserve/handler/adminroutes.go
315+index 8701ae3..f4acd64 100644
316+--- a/internal/httpserve/handler/adminroutes.go
317++++ b/internal/httpserve/handler/adminroutes.go
318+@@ -1,8 +1,6 @@
319+ package handler
320+
321+ import (
322+- "fmt"
323+-
324+ "github.com/bnema/gordon/internal/app"
325+ "github.com/bnema/gordon/internal/gotemplate/render"
326+ "github.com/bnema/gordon/internal/httpserve/middleware"
@@ -0,0 +1,326 @@
1+diff --git a/:memory b/:memory
2+new file mode 100644
3+index 0000000..9760229
4+Binary files /dev/null and b/:memory differ
5+diff --git a/cmd/main.go b/cmd/main.go
6+index 6a95d32..b8e2cd4 100644
7+--- a/cmd/main.go
8++++ b/cmd/main.go
9+@@ -4,17 +4,30 @@ import (
10+ "fmt"
11+ "log/slog"
12+
13++ "github.com/labstack/echo/v4"
14++
15+ "github.com/bnema/gordon/internal/app"
16+ "github.com/bnema/gordon/internal/httpserve"
17+- "github.com/labstack/echo/v4"
18+ )
19+
20+ func main() {
21+ a := app.NewApp()
22++
23++ // Initialize database
24++ db, err := app.InitializeDB(a)
25++ if err != nil {
26++ slog.Error("Failed to load database", err)
27++ } else {
28++ slog.Info("Database loaded")
29++ }
30++
31++ fmt.Println(db)
32++
33+ e := echo.New()
34+ e.HideBanner = true
35+ e.HidePort = true
36+ e = httpserve.RegisterRoutes(e, a)
37+ slog.Info("Starting server", "port", a.HttpPort)
38+ slog.Error("Server error", e.Start(fmt.Sprintf(":%d", a.HttpPort)))
39++
40+ }
41+diff --git a/go.mod b/go.mod
42+index 35e2d3c..8a3ac59 100644
43+--- a/go.mod
44++++ b/go.mod
45+@@ -24,6 +24,7 @@ require (
46+ github.com/gorilla/securecookie v1.1.1 // indirect
47+ github.com/gorilla/sessions v1.2.1 // indirect
48+ github.com/klauspost/compress v1.16.7 // indirect
49++ github.com/kr/text v0.1.0 // indirect
50+ github.com/labstack/gommon v0.4.0 // indirect
51+ github.com/mattn/go-colorable v0.1.13 // indirect
52+ github.com/mattn/go-isatty v0.0.19 // indirect
53+@@ -35,6 +36,7 @@ require (
54+ github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
55+ github.com/opencontainers/runc v1.1.9 // indirect
56+ github.com/pkg/errors v0.9.1 // indirect
57++ github.com/rogpeppe/go-internal v1.11.0 // indirect
58+ github.com/sirupsen/logrus v1.9.3 // indirect
59+ github.com/valyala/bytebufferpool v1.0.0 // indirect
60+ github.com/valyala/fasttemplate v1.2.2 // indirect
61+@@ -45,5 +47,6 @@ require (
62+ golang.org/x/text v0.11.0 // indirect
63+ golang.org/x/time v0.3.0 // indirect
64+ golang.org/x/tools v0.7.0 // indirect
65++ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
66+ gotest.tools/v3 v3.5.0 // indirect
67+ )
68+diff --git a/go.sum b/go.sum
69+index 7b9b857..17b0bcd 100644
70+--- a/go.sum
71++++ b/go.sum
72+@@ -39,6 +39,11 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
73+ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
74+ github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
75+ github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
76++github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
77++github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
78++github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
79++github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
80++github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
81+ github.com/labstack/echo-contrib v0.15.0 h1:9K+oRU265y4Mu9zpRDv3X+DGTqUALY6oRHCSZZKCRVU=
82+ github.com/labstack/echo-contrib v0.15.0/go.mod h1:lei+qt5CLB4oa7VHTE0yEfQSEB9XTJI1LUqko9UWvo4=
83+ github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
84+@@ -72,6 +77,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
85+ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
86+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
87+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
88++github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
89++github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
90+ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
91+ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
92+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
93+@@ -153,8 +160,9 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
94+ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
95+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
96+ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
97+-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
98+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
99++gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
100++gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
101+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
102+ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
103+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
104+diff --git a/internal/app/app.go b/internal/app/app.go
105+index c6f49ac..1dd44b8 100644
106+--- a/internal/app/app.go
107++++ b/internal/app/app.go
108+@@ -27,6 +27,7 @@ const (
109+ type App struct {
110+ TemplateFS fs.FS
111+ PublicFS fs.FS
112++ DB interface{}
113+ BuildVersion string
114+ BuildDir string
115+ DBDir string
116+diff --git a/internal/app/sqlite.go b/internal/app/sqlite.go
117+index 9f1b1ab..ddc6a1e 100644
118+--- a/internal/app/sqlite.go
119++++ b/internal/app/sqlite.go
120+@@ -5,58 +5,78 @@ import (
121+ "database/sql"
122+ "fmt"
123+ "os"
124++ "path/filepath"
125+
126+- sqlite3 "github.com/mattn/go-sqlite3"
127++ "github.com/bnema/gordon/internal/database/migrate"
128++ "github.com/mattn/go-sqlite3"
129+ )
130+
131+-// Initialize in-memory SQLite database and returns its connection
132+-func InitDB() (db *sql.DB, err error) {
133+- db, err = sql.Open("sqlite3", "file::memory?cache=shared")
134+- if err != nil {
135+- return nil, fmt.Errorf("failed to open in-memory database: %v", err)
136+- }
137+- return
138+-}
139+-
140+-// CloseDB closes the database connection and backs up the in-memory database to disk
141+-func CloseDB(db *sql.DB, a *App) error {
142+- if err := BackupDB(db, a); err != nil {
143+- return err
144+- }
145++// InitializeDB initializes the SQLite database. If a database file exists on disk, it loads it into memory.
146++// Otherwise, it creates a new in-memory database and bootstraps it.
147++func InitializeDB(a *App) (*sql.DB, error) {
148++ dbfp := a.GetDBFilePath()
149+
150+- if err := db.Close(); err != nil {
151+- return fmt.Errorf("failed to close database: %v", err)
152++ // Check if the directory exists, if not create it
153++ dir := filepath.Dir(a.DBDir)
154++ fmt.Println("dir", dir)
155++ if _, err := os.Stat(dir); os.IsNotExist(err) {
156++ err = os.MkdirAll(dir, 0755)
157++ if err != nil {
158++ return nil, fmt.Errorf("failed to create directory: %v", err)
159++ }
160+ }
161+
162+- return nil
163+-}
164++ // If the database file doesn't exist on disk
165++ if _, err := os.Stat(dbfp); os.IsNotExist(err) {
166++ memDB, err := initInMemoryDB()
167++ if err != nil {
168++ return nil, err
169++ }
170+
171+-// LoadDBIntoMemory loads the database from disk into memory
172+-func LoadDBIntoMemory(a *App) (*sql.DB, error) {
173+- dbfp := a.GetDBFilePath()
174++ // Run the bootstrap
175++ RunBootstrap(memDB)
176+
177+- if _, err := os.Stat(dbfp); err != nil {
178+- return nil, nil
179++ // Explicitly backup the in-memory database to disk
180++ err = BackupDB(memDB, a)
181++ if err != nil {
182++ return nil, fmt.Errorf("failed to backup in-memory database to disk: %v", err)
183++ }
184+ }
185+
186+- memDB, err := InitDB()
187++ memDB, err := initInMemoryDB()
188+ if err != nil {
189+ return nil, err
190+ }
191+-
192+ diskDB, err := sql.Open("sqlite3", dbfp)
193+ if err != nil {
194+ return nil, fmt.Errorf("failed to open database file: %v", err)
195+ }
196++ defer diskDB.Close()
197+
198+ if err := transferData(diskDB, memDB); err != nil {
199+ return nil, err
200+ }
201+-
202+ return memDB, nil
203+ }
204+
205+-// transferData transfers data from srcDB to dstDB
206++func initInMemoryDB() (*sql.DB, error) {
207++ db, err := sql.Open("sqlite3", "file::memory?cache=shared")
208++ if err != nil {
209++ return nil, fmt.Errorf("failed to open in-memory database: %v", err)
210++ }
211++ return db, nil
212++}
213++
214++func CloseAndBackupDB(db *sql.DB, a *App) error {
215++ if err := BackupDB(db, a); err != nil {
216++ return err
217++ }
218++ if err := db.Close(); err != nil {
219++ return fmt.Errorf("failed to close database: %v", err)
220++ }
221++ return nil
222++}
223++
224+ func transferData(srcDB, dstDB *sql.DB) error {
225+ srcConn, err := srcDB.Conn(context.Background())
226+ if err != nil {
227+@@ -74,35 +94,50 @@ func transferData(srcDB, dstDB *sql.DB) error {
228+ return dstConn.Raw(func(dstRawConn interface{}) error {
229+ srcSqlite3Conn := srcRawConn.(*sqlite3.SQLiteConn)
230+ dstSqlite3Conn := dstRawConn.(*sqlite3.SQLiteConn)
231+-
232+ _, err := srcSqlite3Conn.Backup("main", dstSqlite3Conn, "main")
233+ return err
234+ })
235+ })
236+ }
237+
238+-// BackupDB backs up the in-memory database to disk
239+ func BackupDB(db *sql.DB, a *App) error {
240+ dbfp := a.GetDBFilePath()
241+-
242+ conn, err := db.Conn(context.Background())
243+ if err != nil {
244+ return fmt.Errorf("failed to get underlying database connection: %v", err)
245+ }
246++ defer conn.Close()
247+
248+ return conn.Raw(func(sqliteConn interface{}) error {
249+ sqlite3Conn := sqliteConn.(*sqlite3.SQLiteConn)
250+- backupDB, err := sqlite3Conn.Backup(dbfp, sqlite3Conn, "main")
251++ diskDB, err := sql.Open("sqlite3", dbfp)
252++ fmt.Println("diskDB", diskDB)
253++ if err != nil {
254++ return fmt.Errorf("failed to open destination database file: %v", err)
255++ }
256++ defer diskDB.Close()
257++
258++ diskConn, err := diskDB.Conn(context.Background())
259+ if err != nil {
260+- return fmt.Errorf("failed to create backup: %v", err)
261++ return fmt.Errorf("failed to get connection from diskDB: %v", err)
262+ }
263++ defer diskConn.Close()
264++
265++ var backupDB *sqlite3.SQLiteBackup
266++ err = diskConn.Raw(func(diskRawConn interface{}) error {
267++ backupDB, err = sqlite3Conn.Backup("main", diskRawConn.(*sqlite3.SQLiteConn), "main")
268++ return err
269++ })
270+ defer backupDB.Close()
271+
272+ _, err = backupDB.Step(-1)
273+ if err != nil {
274+ return fmt.Errorf("failed to backup in-memory database: %v", err)
275+ }
276+-
277+ return nil
278+ })
279+ }
280++
281++func RunBootstrap(db *sql.DB) error {
282++ return migrate.CreateUserTable(db)
283++}
284+diff --git a/internal/database/migrate/bootstrapdb.go b/internal/database/migrate/bootstrapdb.go
285+new file mode 100644
286+index 0000000..1f53246
287+--- /dev/null
288++++ b/internal/database/migrate/bootstrapdb.go
289+@@ -0,0 +1,24 @@
290++package migrate
291++
292++import (
293++ "database/sql"
294++ "fmt"
295++)
296++
297++// createUserTable creates the 'user' table in the provided SQLite database.
298++func CreateUserTable(db *sql.DB) error {
299++ createTableSQL := `
300++ CREATE TABLE user (
301++ id INTEGER PRIMARY KEY AUTOINCREMENT,
302++ username TEXT NOT NULL,
303++ password TEXT NOT NULL, -- This should be hashed and salted in a real-world scenario.
304++ oauth_token TEXT
305++ );`
306++
307++ _, err := db.Exec(createTableSQL)
308++ if err != nil {
309++ return fmt.Errorf("failed to create user table: %v", err)
310++ }
311++
312++ return nil
313++}
314+diff --git a/internal/httpserve/handler/adminroutes.go b/internal/httpserve/handler/adminroutes.go
315+index 8701ae3..f4acd64 100644
316+--- a/internal/httpserve/handler/adminroutes.go
317++++ b/internal/httpserve/handler/adminroutes.go
318+@@ -1,8 +1,6 @@
319+ package handler
320+
321+ import (
322+- "fmt"
323+-
324+ "github.com/bnema/gordon/internal/app"
325+ "github.com/bnema/gordon/internal/gotemplate/render"
326+ "github.com/bnema/gordon/internal/httpserve/middleware"
M cmd/main.go +30 -8
@@ -1,8 +1,12 @@
11 package main
22
33 import (
4+ "database/sql"
45 "fmt"
5- "log/slog"
6+ "log"
7+ "os"
8+ "os/signal"
9+ "syscall"
610
711 "github.com/labstack/echo/v4"
812
@@ -10,24 +14,42 @@ import (
1014 "github.com/bnema/gordon/internal/httpserve"
1115 )
1216
17+func cleanup(a *app.App, memDb *sql.DB) {
18+ if err := app.CloseAndBackupDB(a, memDb); err != nil {
19+ log.Fatal("Failed to close and backup database:", err)
20+ }
21+}
22+
1323 func main() {
1424 a := app.NewApp()
1525
1626 // Initialize database
17- db, err := app.InitializeDB(a)
27+ memDb, err := app.InitializeDB(a)
1828 if err != nil {
19- slog.Error("Failed to load database", err)
20- } else {
21- slog.Info("Database loaded")
29+ log.Fatal(err)
2230 }
2331
24- fmt.Println(db)
32+ // Pass memDb to the app with the rest of the configs
33+ a.DB = memDb
34+
35+ // Setup a channel to capture termination signals
36+ sigs := make(chan os.Signal, 1)
37+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
38+
39+ go func() {
40+ sig := <-sigs
41+ log.Println("Received signal:", sig)
42+ cleanup(a, memDb) // <- Call the cleanup function
43+ os.Exit(0)
44+ }()
2545
2646 e := echo.New()
2747 e.HideBanner = true
2848 e.HidePort = true
2949 e = httpserve.RegisterRoutes(e, a)
30- slog.Info("Starting server", "port", a.HttpPort)
31- slog.Error("Server error", e.Start(fmt.Sprintf(":%d", a.HttpPort)))
3250
51+ log.Println("Starting server on port", a.HttpPort)
52+ if err := e.Start(fmt.Sprintf(":%d", a.HttpPort)); err != nil {
53+ log.Fatal("Server error:", err)
54+ }
3355 }
@@ -1,8 +1,12 @@
1 package main1 package main
2 2
3 import (3 import (
4+ "database/sql"
4 "fmt"5 "fmt"
5- "log/slog"6+ "log"
7+ "os"
8+ "os/signal"
9+ "syscall"
6 10
7 "github.com/labstack/echo/v4"11 "github.com/labstack/echo/v4"
8 12
@@ -10,24 +14,42 @@ import (
10 "github.com/bnema/gordon/internal/httpserve"14 "github.com/bnema/gordon/internal/httpserve"
11 )15 )
12 16
17+func cleanup(a *app.App, memDb *sql.DB) {
18+ if err := app.CloseAndBackupDB(a, memDb); err != nil {
19+ log.Fatal("Failed to close and backup database:", err)
20+ }
21+}
22+
13 func main() {23 func main() {
14 a := app.NewApp()24 a := app.NewApp()
15 25
16 // Initialize database26 // Initialize database
17- db, err := app.InitializeDB(a)27+ memDb, err := app.InitializeDB(a)
18 if err != nil {28 if err != nil {
19- slog.Error("Failed to load database", err)29+ log.Fatal(err)
20- } else {
21- slog.Info("Database loaded")
22 }30 }
23 31
24- fmt.Println(db)32+ // Pass memDb to the app with the rest of the configs
33+ a.DB = memDb
34+
35+ // Setup a channel to capture termination signals
36+ sigs := make(chan os.Signal, 1)
37+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
38+
39+ go func() {
40+ sig := <-sigs
41+ log.Println("Received signal:", sig)
42+ cleanup(a, memDb) // <- Call the cleanup function
43+ os.Exit(0)
44+ }()
25 45
26 e := echo.New()46 e := echo.New()
27 e.HideBanner = true47 e.HideBanner = true
28 e.HidePort = true48 e.HidePort = true
29 e = httpserve.RegisterRoutes(e, a)49 e = httpserve.RegisterRoutes(e, a)
30- slog.Info("Starting server", "port", a.HttpPort)
31- slog.Error("Server error", e.Start(fmt.Sprintf(":%d", a.HttpPort)))
32 50
51+ log.Println("Starting server on port", a.HttpPort)
52+ if err := e.Start(fmt.Sprintf(":%d", a.HttpPort)); err != nil {
53+ log.Fatal("Server error:", err)
54+ }
33 }55 }
M internal/app/app.go +17 -20
@@ -1,8 +1,8 @@
11 package app
22
33 import (
4+ "database/sql"
45 "io/fs"
5- "path/filepath"
66
77 "github.com/bnema/gordon/internal/gotemplate"
88 "github.com/bnema/gordon/internal/webui"
@@ -11,8 +11,6 @@ import (
1111 const (
1212 BuildVersion = "0.0.2"
1313 BuildDir = "tmp"
14- DBDir = "tmp/data"
15- DBFilename = "sqlite.db"
1614 // Those configs should be read from a config file
1715 HttpPort = 1323
1816 TopDomain = "example.com"
@@ -25,20 +23,22 @@ const (
2523 )
2624
2725 type App struct {
28- TemplateFS fs.FS
29- PublicFS fs.FS
30- DB interface{}
31- BuildVersion string
32- BuildDir string
33- DBDir string
34- DBFilename string
35- TopDomain string
36- SubDomain string
37- AdminPath string
38- DockerSock string
39- PodmanEnable bool
40- PodmanSock string
41- HttpPort int16
26+ TemplateFS fs.FS
27+ PublicFS fs.FS
28+ BuildVersion string
29+ BuildDir string
30+ DBDir string
31+ DBFilename string
32+ DBPath string
33+ InitialChecksum string
34+ DB *sql.DB
35+ TopDomain string
36+ SubDomain string
37+ AdminPath string
38+ DockerSock string
39+ PodmanEnable bool
40+ PodmanSock string
41+ HttpPort int16
4242 }
4343
4444 func NewApp() *App {
@@ -63,6 +63,3 @@ func NewDockerClient() *App {
6363 PodmanSock: PodmanSock,
6464 }
6565 }
66-func (a *App) GetDBFilePath() string {
67- return filepath.Join(a.DBDir, a.DBFilename)
68-}
@@ -1,8 +1,8 @@
1 package app1 package app
2 2
3 import (3 import (
4+ "database/sql"
4 "io/fs"5 "io/fs"
5- "path/filepath"
6 6
7 "github.com/bnema/gordon/internal/gotemplate"7 "github.com/bnema/gordon/internal/gotemplate"
8 "github.com/bnema/gordon/internal/webui"8 "github.com/bnema/gordon/internal/webui"
@@ -11,8 +11,6 @@ import (
11 const (11 const (
12 BuildVersion = "0.0.2"12 BuildVersion = "0.0.2"
13 BuildDir = "tmp"13 BuildDir = "tmp"
14- DBDir = "tmp/data"
15- DBFilename = "sqlite.db"
16 // Those configs should be read from a config file14 // Those configs should be read from a config file
17 HttpPort = 132315 HttpPort = 1323
18 TopDomain = "example.com"16 TopDomain = "example.com"
@@ -25,20 +23,22 @@ const (
25 )23 )
26 24
27 type App struct {25 type App struct {
28- TemplateFS fs.FS26+ TemplateFS fs.FS
29- PublicFS fs.FS27+ PublicFS fs.FS
30- DB interface{}28+ BuildVersion string
31- BuildVersion string29+ BuildDir string
32- BuildDir string30+ DBDir string
33- DBDir string31+ DBFilename string
34- DBFilename string32+ DBPath string
35- TopDomain string33+ InitialChecksum string
36- SubDomain string34+ DB *sql.DB
37- AdminPath string35+ TopDomain string
38- DockerSock string36+ SubDomain string
39- PodmanEnable bool37+ AdminPath string
40- PodmanSock string38+ DockerSock string
41- HttpPort int1639+ PodmanEnable bool
40+ PodmanSock string
41+ HttpPort int16
42 }42 }
43 43
44 func NewApp() *App {44 func NewApp() *App {
@@ -63,6 +63,3 @@ func NewDockerClient() *App {
63 PodmanSock: PodmanSock,63 PodmanSock: PodmanSock,
64 }64 }
65 }65 }
66-func (a *App) GetDBFilePath() string {
67- return filepath.Join(a.DBDir, a.DBFilename)
68-}
D internal/app/sqlite.go +0 -143
@@ -1,143 +0,0 @@
1-package app
2-
3-import (
4- "context"
5- "database/sql"
6- "fmt"
7- "os"
8- "path/filepath"
9-
10- "github.com/bnema/gordon/internal/database/migrate"
11- "github.com/mattn/go-sqlite3"
12-)
13-
14-// InitializeDB initializes the SQLite database. If a database file exists on disk, it loads it into memory.
15-// Otherwise, it creates a new in-memory database and bootstraps it.
16-func InitializeDB(a *App) (*sql.DB, error) {
17- dbfp := a.GetDBFilePath()
18-
19- // Check if the directory exists, if not create it
20- dir := filepath.Dir(a.DBDir)
21- fmt.Println("dir", dir)
22- if _, err := os.Stat(dir); os.IsNotExist(err) {
23- err = os.MkdirAll(dir, 0755)
24- if err != nil {
25- return nil, fmt.Errorf("failed to create directory: %v", err)
26- }
27- }
28-
29- // If the database file doesn't exist on disk
30- if _, err := os.Stat(dbfp); os.IsNotExist(err) {
31- memDB, err := initInMemoryDB()
32- if err != nil {
33- return nil, err
34- }
35-
36- // Run the bootstrap
37- RunBootstrap(memDB)
38-
39- // Explicitly backup the in-memory database to disk
40- err = BackupDB(memDB, a)
41- if err != nil {
42- return nil, fmt.Errorf("failed to backup in-memory database to disk: %v", err)
43- }
44- }
45-
46- memDB, err := initInMemoryDB()
47- if err != nil {
48- return nil, err
49- }
50- diskDB, err := sql.Open("sqlite3", dbfp)
51- if err != nil {
52- return nil, fmt.Errorf("failed to open database file: %v", err)
53- }
54- defer diskDB.Close()
55-
56- if err := transferData(diskDB, memDB); err != nil {
57- return nil, err
58- }
59- return memDB, nil
60-}
61-
62-func initInMemoryDB() (*sql.DB, error) {
63- db, err := sql.Open("sqlite3", "file::memory?cache=shared")
64- if err != nil {
65- return nil, fmt.Errorf("failed to open in-memory database: %v", err)
66- }
67- return db, nil
68-}
69-
70-func CloseAndBackupDB(db *sql.DB, a *App) error {
71- if err := BackupDB(db, a); err != nil {
72- return err
73- }
74- if err := db.Close(); err != nil {
75- return fmt.Errorf("failed to close database: %v", err)
76- }
77- return nil
78-}
79-
80-func transferData(srcDB, dstDB *sql.DB) error {
81- srcConn, err := srcDB.Conn(context.Background())
82- if err != nil {
83- return fmt.Errorf("failed to get connection from srcDB: %v", err)
84- }
85- defer srcConn.Close()
86-
87- dstConn, err := dstDB.Conn(context.Background())
88- if err != nil {
89- return fmt.Errorf("failed to get connection from dstDB: %v", err)
90- }
91- defer dstConn.Close()
92-
93- return srcConn.Raw(func(srcRawConn interface{}) error {
94- return dstConn.Raw(func(dstRawConn interface{}) error {
95- srcSqlite3Conn := srcRawConn.(*sqlite3.SQLiteConn)
96- dstSqlite3Conn := dstRawConn.(*sqlite3.SQLiteConn)
97- _, err := srcSqlite3Conn.Backup("main", dstSqlite3Conn, "main")
98- return err
99- })
100- })
101-}
102-
103-func BackupDB(db *sql.DB, a *App) error {
104- dbfp := a.GetDBFilePath()
105- conn, err := db.Conn(context.Background())
106- if err != nil {
107- return fmt.Errorf("failed to get underlying database connection: %v", err)
108- }
109- defer conn.Close()
110-
111- return conn.Raw(func(sqliteConn interface{}) error {
112- sqlite3Conn := sqliteConn.(*sqlite3.SQLiteConn)
113- diskDB, err := sql.Open("sqlite3", dbfp)
114- fmt.Println("diskDB", diskDB)
115- if err != nil {
116- return fmt.Errorf("failed to open destination database file: %v", err)
117- }
118- defer diskDB.Close()
119-
120- diskConn, err := diskDB.Conn(context.Background())
121- if err != nil {
122- return fmt.Errorf("failed to get connection from diskDB: %v", err)
123- }
124- defer diskConn.Close()
125-
126- var backupDB *sqlite3.SQLiteBackup
127- err = diskConn.Raw(func(diskRawConn interface{}) error {
128- backupDB, err = sqlite3Conn.Backup("main", diskRawConn.(*sqlite3.SQLiteConn), "main")
129- return err
130- })
131- defer backupDB.Close()
132-
133- _, err = backupDB.Step(-1)
134- if err != nil {
135- return fmt.Errorf("failed to backup in-memory database: %v", err)
136- }
137- return nil
138- })
139-}
140-
141-func RunBootstrap(db *sql.DB) error {
142- return migrate.CreateUserTable(db)
143-}
@@ -1,143 +0,0 @@
1-package app
2-
3-import (
4- "context"
5- "database/sql"
6- "fmt"
7- "os"
8- "path/filepath"
9-
10- "github.com/bnema/gordon/internal/database/migrate"
11- "github.com/mattn/go-sqlite3"
12-)
13-
14-// InitializeDB initializes the SQLite database. If a database file exists on disk, it loads it into memory.
15-// Otherwise, it creates a new in-memory database and bootstraps it.
16-func InitializeDB(a *App) (*sql.DB, error) {
17- dbfp := a.GetDBFilePath()
18-
19- // Check if the directory exists, if not create it
20- dir := filepath.Dir(a.DBDir)
21- fmt.Println("dir", dir)
22- if _, err := os.Stat(dir); os.IsNotExist(err) {
23- err = os.MkdirAll(dir, 0755)
24- if err != nil {
25- return nil, fmt.Errorf("failed to create directory: %v", err)
26- }
27- }
28-
29- // If the database file doesn't exist on disk
30- if _, err := os.Stat(dbfp); os.IsNotExist(err) {
31- memDB, err := initInMemoryDB()
32- if err != nil {
33- return nil, err
34- }
35-
36- // Run the bootstrap
37- RunBootstrap(memDB)
38-
39- // Explicitly backup the in-memory database to disk
40- err = BackupDB(memDB, a)
41- if err != nil {
42- return nil, fmt.Errorf("failed to backup in-memory database to disk: %v", err)
43- }
44- }
45-
46- memDB, err := initInMemoryDB()
47- if err != nil {
48- return nil, err
49- }
50- diskDB, err := sql.Open("sqlite3", dbfp)
51- if err != nil {
52- return nil, fmt.Errorf("failed to open database file: %v", err)
53- }
54- defer diskDB.Close()
55-
56- if err := transferData(diskDB, memDB); err != nil {
57- return nil, err
58- }
59- return memDB, nil
60-}
61-
62-func initInMemoryDB() (*sql.DB, error) {
63- db, err := sql.Open("sqlite3", "file::memory?cache=shared")
64- if err != nil {
65- return nil, fmt.Errorf("failed to open in-memory database: %v", err)
66- }
67- return db, nil
68-}
69-
70-func CloseAndBackupDB(db *sql.DB, a *App) error {
71- if err := BackupDB(db, a); err != nil {
72- return err
73- }
74- if err := db.Close(); err != nil {
75- return fmt.Errorf("failed to close database: %v", err)
76- }
77- return nil
78-}
79-
80-func transferData(srcDB, dstDB *sql.DB) error {
81- srcConn, err := srcDB.Conn(context.Background())
82- if err != nil {
83- return fmt.Errorf("failed to get connection from srcDB: %v", err)
84- }
85- defer srcConn.Close()
86-
87- dstConn, err := dstDB.Conn(context.Background())
88- if err != nil {
89- return fmt.Errorf("failed to get connection from dstDB: %v", err)
90- }
91- defer dstConn.Close()
92-
93- return srcConn.Raw(func(srcRawConn interface{}) error {
94- return dstConn.Raw(func(dstRawConn interface{}) error {
95- srcSqlite3Conn := srcRawConn.(*sqlite3.SQLiteConn)
96- dstSqlite3Conn := dstRawConn.(*sqlite3.SQLiteConn)
97- _, err := srcSqlite3Conn.Backup("main", dstSqlite3Conn, "main")
98- return err
99- })
100- })
101-}
102-
103-func BackupDB(db *sql.DB, a *App) error {
104- dbfp := a.GetDBFilePath()
105- conn, err := db.Conn(context.Background())
106- if err != nil {
107- return fmt.Errorf("failed to get underlying database connection: %v", err)
108- }
109- defer conn.Close()
110-
111- return conn.Raw(func(sqliteConn interface{}) error {
112- sqlite3Conn := sqliteConn.(*sqlite3.SQLiteConn)
113- diskDB, err := sql.Open("sqlite3", dbfp)
114- fmt.Println("diskDB", diskDB)
115- if err != nil {
116- return fmt.Errorf("failed to open destination database file: %v", err)
117- }
118- defer diskDB.Close()
119-
120- diskConn, err := diskDB.Conn(context.Background())
121- if err != nil {
122- return fmt.Errorf("failed to get connection from diskDB: %v", err)
123- }
124- defer diskConn.Close()
125-
126- var backupDB *sqlite3.SQLiteBackup
127- err = diskConn.Raw(func(diskRawConn interface{}) error {
128- backupDB, err = sqlite3Conn.Backup("main", diskRawConn.(*sqlite3.SQLiteConn), "main")
129- return err
130- })
131- defer backupDB.Close()
132-
133- _, err = backupDB.Step(-1)
134- if err != nil {
135- return fmt.Errorf("failed to backup in-memory database: %v", err)
136- }
137- return nil
138- })
139-}
140-
141-func RunBootstrap(db *sql.DB) error {
142- return migrate.CreateUserTable(db)
143-}
A internal/app/sqlite_test.go +140 -0
@@ -0,0 +1,140 @@
1+package app
2+
3+import (
4+ "database/sql"
5+ "os"
6+ "testing"
7+)
8+
9+// test for CloseAndBackupDB with database modifications (checksum should be different and backup file should be created)
10+func TestCloseAndBackupDBWithModifications(t *testing.T) {
11+ // Ensure the directory exists
12+ err := os.MkdirAll("./tmp/data", 0755)
13+ if err != nil {
14+ t.Fatalf("Failed to create directory: %v", err)
15+ }
16+
17+ // Create a temporary file for testing
18+ tempFile, err := os.CreateTemp("./tmp/data", "sqlite.db")
19+ if err != nil {
20+ t.Fatalf("Failed to create temp file: %v", err)
21+ }
22+ tempFile.Close()
23+
24+ app := &App{
25+ DBFilename: tempFile.Name(),
26+ }
27+
28+ // Initialize in-memory database
29+ memDb, err := sql.Open("sqlite3", ":memory:")
30+ if err != nil {
31+ t.Fatalf("Failed to open in-memory database: %v", err)
32+ }
33+ defer memDb.Close()
34+
35+ // Create table and insert some data
36+ _, err = memDb.Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
37+ if err != nil {
38+ t.Fatalf("Failed to create table: %v", err)
39+ }
40+ _, err = memDb.Exec("INSERT INTO users (name) VALUES ('Alice'), ('Bob')")
41+ if err != nil {
42+ t.Fatalf("Failed to insert data: %v", err)
43+ }
44+
45+ // Generate initial checksum
46+ initialChecksum, err := GenerateDBChecksum(memDb)
47+ if err != nil {
48+ t.Fatalf("Failed to generate initial checksum: %v", err)
49+ }
50+
51+ // Validate: Checksum should remain the same
52+ finalChecksum, err := GenerateDBChecksum(memDb)
53+ if err != nil {
54+ t.Fatalf("Failed to generate final checksum: %v", err)
55+ }
56+ if initialChecksum != finalChecksum {
57+ t.Fatalf("Checksum mismatch. Expected: %s, Got: %s", initialChecksum, finalChecksum)
58+ }
59+
60+ // Modify the database
61+ _, err = memDb.Exec("INSERT INTO users (name) VALUES ('Charlie')")
62+ if err != nil {
63+ t.Fatalf("Failed to insert data: %v", err)
64+ }
65+
66+ // Validate: Checksum should be different
67+ finalChecksum, err = GenerateDBChecksum(memDb)
68+ if err != nil {
69+ t.Fatalf("Failed to generate final checksum: %v", err)
70+ }
71+ if initialChecksum == finalChecksum {
72+ t.Fatalf("Checksum mismatch. Expected: %s, Got: %s", initialChecksum, finalChecksum)
73+ }
74+
75+ // Test: CloseAndBackupDB with modification
76+ err = CloseAndBackupDB(app, memDb)
77+ if err != nil {
78+ t.Fatalf("Failed in CloseAndBackupDB: %v", err)
79+ }
80+
81+}
82+
83+// test for CloseAndBackupDB with NO database modifications (checksum should remain the same and no backup file should be created)
84+func TestCloseAndBackupDBWithNoModifications(t *testing.T) {
85+ // Ensure the directory exists
86+ err := os.MkdirAll("./tmp/data", 0755)
87+ if err != nil {
88+ t.Fatalf("Failed to create directory: %v", err)
89+ }
90+
91+ // Create a temporary file for testing
92+ tempFile, err := os.CreateTemp("./tmp/data", "sqlite.db")
93+ if err != nil {
94+ t.Fatalf("Failed to create temp file: %v", err)
95+ }
96+ tempFile.Close()
97+
98+ app := &App{
99+ DBFilename: tempFile.Name(),
100+ }
101+
102+ // Initialize in-memory database
103+ memDb, err := sql.Open("sqlite3", ":memory:")
104+ if err != nil {
105+ t.Fatalf("Failed to open in-memory database: %v", err)
106+ }
107+ defer memDb.Close()
108+
109+ // Create table and insert some data
110+ _, err = memDb.Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
111+ if err != nil {
112+ t.Fatalf("Failed to create table: %v", err)
113+ }
114+ _, err = memDb.Exec("INSERT INTO users (name) VALUES ('Alice'), ('Bob')")
115+ if err != nil {
116+ t.Fatalf("Failed to insert data: %v", err)
117+ }
118+
119+ // Generate initial checksum
120+ initialChecksum, err := GenerateDBChecksum(memDb)
121+ if err != nil {
122+ t.Fatalf("Failed to generate initial checksum: %v", err)
123+ }
124+
125+ // Validate: Checksum should remain the same
126+ finalChecksum, err := GenerateDBChecksum(memDb)
127+ if err != nil {
128+ t.Fatalf("Failed to generate final checksum: %v", err)
129+ }
130+ if initialChecksum != finalChecksum {
131+ t.Fatalf("Checksum mismatch. Expected: %s, Got: %s", initialChecksum, finalChecksum)
132+ }
133+
134+ // Test: CloseAndBackupDB with modification
135+ err = CloseAndBackupDB(app, memDb)
136+ if err != nil {
137+ t.Fatalf("Failed in CloseAndBackupDB: %v", err)
138+ }
139+
140+}
@@ -0,0 +1,140 @@
1+package app
2+
3+import (
4+ "database/sql"
5+ "os"
6+ "testing"
7+)
8+
9+// test for CloseAndBackupDB with database modifications (checksum should be different and backup file should be created)
10+func TestCloseAndBackupDBWithModifications(t *testing.T) {
11+ // Ensure the directory exists
12+ err := os.MkdirAll("./tmp/data", 0755)
13+ if err != nil {
14+ t.Fatalf("Failed to create directory: %v", err)
15+ }
16+
17+ // Create a temporary file for testing
18+ tempFile, err := os.CreateTemp("./tmp/data", "sqlite.db")
19+ if err != nil {
20+ t.Fatalf("Failed to create temp file: %v", err)
21+ }
22+ tempFile.Close()
23+
24+ app := &App{
25+ DBFilename: tempFile.Name(),
26+ }
27+
28+ // Initialize in-memory database
29+ memDb, err := sql.Open("sqlite3", ":memory:")
30+ if err != nil {
31+ t.Fatalf("Failed to open in-memory database: %v", err)
32+ }
33+ defer memDb.Close()
34+
35+ // Create table and insert some data
36+ _, err = memDb.Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
37+ if err != nil {
38+ t.Fatalf("Failed to create table: %v", err)
39+ }
40+ _, err = memDb.Exec("INSERT INTO users (name) VALUES ('Alice'), ('Bob')")
41+ if err != nil {
42+ t.Fatalf("Failed to insert data: %v", err)
43+ }
44+
45+ // Generate initial checksum
46+ initialChecksum, err := GenerateDBChecksum(memDb)
47+ if err != nil {
48+ t.Fatalf("Failed to generate initial checksum: %v", err)
49+ }
50+
51+ // Validate: Checksum should remain the same
52+ finalChecksum, err := GenerateDBChecksum(memDb)
53+ if err != nil {
54+ t.Fatalf("Failed to generate final checksum: %v", err)
55+ }
56+ if initialChecksum != finalChecksum {
57+ t.Fatalf("Checksum mismatch. Expected: %s, Got: %s", initialChecksum, finalChecksum)
58+ }
59+
60+ // Modify the database
61+ _, err = memDb.Exec("INSERT INTO users (name) VALUES ('Charlie')")
62+ if err != nil {
63+ t.Fatalf("Failed to insert data: %v", err)
64+ }
65+
66+ // Validate: Checksum should be different
67+ finalChecksum, err = GenerateDBChecksum(memDb)
68+ if err != nil {
69+ t.Fatalf("Failed to generate final checksum: %v", err)
70+ }
71+ if initialChecksum == finalChecksum {
72+ t.Fatalf("Checksum mismatch. Expected: %s, Got: %s", initialChecksum, finalChecksum)
73+ }
74+
75+ // Test: CloseAndBackupDB with modification
76+ err = CloseAndBackupDB(app, memDb)
77+ if err != nil {
78+ t.Fatalf("Failed in CloseAndBackupDB: %v", err)
79+ }
80+
81+}
82+
83+// test for CloseAndBackupDB with NO database modifications (checksum should remain the same and no backup file should be created)
84+func TestCloseAndBackupDBWithNoModifications(t *testing.T) {
85+ // Ensure the directory exists
86+ err := os.MkdirAll("./tmp/data", 0755)
87+ if err != nil {
88+ t.Fatalf("Failed to create directory: %v", err)
89+ }
90+
91+ // Create a temporary file for testing
92+ tempFile, err := os.CreateTemp("./tmp/data", "sqlite.db")
93+ if err != nil {
94+ t.Fatalf("Failed to create temp file: %v", err)
95+ }
96+ tempFile.Close()
97+
98+ app := &App{
99+ DBFilename: tempFile.Name(),
100+ }
101+
102+ // Initialize in-memory database
103+ memDb, err := sql.Open("sqlite3", ":memory:")
104+ if err != nil {
105+ t.Fatalf("Failed to open in-memory database: %v", err)
106+ }
107+ defer memDb.Close()
108+
109+ // Create table and insert some data
110+ _, err = memDb.Exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
111+ if err != nil {
112+ t.Fatalf("Failed to create table: %v", err)
113+ }
114+ _, err = memDb.Exec("INSERT INTO users (name) VALUES ('Alice'), ('Bob')")
115+ if err != nil {
116+ t.Fatalf("Failed to insert data: %v", err)
117+ }
118+
119+ // Generate initial checksum
120+ initialChecksum, err := GenerateDBChecksum(memDb)
121+ if err != nil {
122+ t.Fatalf("Failed to generate initial checksum: %v", err)
123+ }
124+
125+ // Validate: Checksum should remain the same
126+ finalChecksum, err := GenerateDBChecksum(memDb)
127+ if err != nil {
128+ t.Fatalf("Failed to generate final checksum: %v", err)
129+ }
130+ if initialChecksum != finalChecksum {
131+ t.Fatalf("Checksum mismatch. Expected: %s, Got: %s", initialChecksum, finalChecksum)
132+ }
133+
134+ // Test: CloseAndBackupDB with modification
135+ err = CloseAndBackupDB(app, memDb)
136+ if err != nil {
137+ t.Fatalf("Failed in CloseAndBackupDB: %v", err)
138+ }
139+
140+}
A internal/app/sqliteclose.go +72 -0
@@ -0,0 +1,72 @@
1+package app
2+
3+import (
4+ "database/sql"
5+ "fmt"
6+ "os"
7+)
8+
9+// GetTableNames returns a list of table names in the database.
10+func GetTableNames(db *sql.DB) ([]string, error) {
11+ rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table'")
12+ if err != nil {
13+ return nil, err
14+ }
15+ defer rows.Close()
16+
17+ var tables []string
18+ for rows.Next() {
19+ var tableName string
20+ if err := rows.Scan(&tableName); err != nil {
21+ return nil, err
22+ }
23+ tables = append(tables, tableName)
24+ }
25+
26+ return tables, nil
27+}
28+
29+// CloseAndBackupDB closes the in-memory database and backup the database file if there is any modification.
30+func CloseAndBackupDB(a *App, memDb *sql.DB) error {
31+ currentChecksum, err := GenerateDBChecksum(memDb)
32+ if err != nil {
33+ return err
34+ }
35+ fmt.Printf("Current checksum: %s\n", currentChecksum)
36+ fmt.Printf("Initial checksum: %s\n", a.InitialChecksum)
37+ if currentChecksum != a.InitialChecksum {
38+ fmt.Printf("Modification has been found in the database. Current checksum: %s\n", currentChecksum)
39+
40+ // Backup the current db file
41+ backupPath := a.GetDiskDBFilePath() + ".backup"
42+ if err := os.Rename(a.GetDiskDBFilePath(), backupPath); err != nil {
43+ return err
44+ }
45+ fmt.Printf("Backup file has been created: %s\n", backupPath)
46+
47+ // Save the in-memory database to disk
48+ _, err = memDb.Exec("ATTACH DATABASE '" + a.GetDiskDBFilePath() + "' AS diskdb")
49+ if err != nil {
50+ return err
51+ }
52+
53+ _, err = memDb.Exec("CREATE TABLE diskdb.users AS SELECT * FROM main.users")
54+ if err != nil {
55+ return err
56+ }
57+
58+ _, err = memDb.Exec("DETACH DATABASE diskdb")
59+ if err != nil {
60+ return err
61+ }
62+ } else {
63+ fmt.Printf("No modification has been found in the database.\n")
64+ }
65+
66+ if err := memDb.Close(); err != nil {
67+ return err
68+ }
69+ fmt.Printf("In-memory database has been closed gracefully.\n")
70+
71+ return nil
72+}
@@ -0,0 +1,72 @@
1+package app
2+
3+import (
4+ "database/sql"
5+ "fmt"
6+ "os"
7+)
8+
9+// GetTableNames returns a list of table names in the database.
10+func GetTableNames(db *sql.DB) ([]string, error) {
11+ rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table'")
12+ if err != nil {
13+ return nil, err
14+ }
15+ defer rows.Close()
16+
17+ var tables []string
18+ for rows.Next() {
19+ var tableName string
20+ if err := rows.Scan(&tableName); err != nil {
21+ return nil, err
22+ }
23+ tables = append(tables, tableName)
24+ }
25+
26+ return tables, nil
27+}
28+
29+// CloseAndBackupDB closes the in-memory database and backup the database file if there is any modification.
30+func CloseAndBackupDB(a *App, memDb *sql.DB) error {
31+ currentChecksum, err := GenerateDBChecksum(memDb)
32+ if err != nil {
33+ return err
34+ }
35+ fmt.Printf("Current checksum: %s\n", currentChecksum)
36+ fmt.Printf("Initial checksum: %s\n", a.InitialChecksum)
37+ if currentChecksum != a.InitialChecksum {
38+ fmt.Printf("Modification has been found in the database. Current checksum: %s\n", currentChecksum)
39+
40+ // Backup the current db file
41+ backupPath := a.GetDiskDBFilePath() + ".backup"
42+ if err := os.Rename(a.GetDiskDBFilePath(), backupPath); err != nil {
43+ return err
44+ }
45+ fmt.Printf("Backup file has been created: %s\n", backupPath)
46+
47+ // Save the in-memory database to disk
48+ _, err = memDb.Exec("ATTACH DATABASE '" + a.GetDiskDBFilePath() + "' AS diskdb")
49+ if err != nil {
50+ return err
51+ }
52+
53+ _, err = memDb.Exec("CREATE TABLE diskdb.users AS SELECT * FROM main.users")
54+ if err != nil {
55+ return err
56+ }
57+
58+ _, err = memDb.Exec("DETACH DATABASE diskdb")
59+ if err != nil {
60+ return err
61+ }
62+ } else {
63+ fmt.Printf("No modification has been found in the database.\n")
64+ }
65+
66+ if err := memDb.Close(); err != nil {
67+ return err
68+ }
69+ fmt.Printf("In-memory database has been closed gracefully.\n")
70+
71+ return nil
72+}
A internal/app/sqliteinit.go +161 -0
@@ -0,0 +1,161 @@
1+package app
2+
3+import (
4+ "crypto/md5"
5+ "database/sql"
6+ "encoding/hex"
7+ "fmt"
8+ "os"
9+ "path/filepath"
10+
11+ "github.com/bnema/gordon/internal/database/migrate"
12+ _ "github.com/mattn/go-sqlite3"
13+)
14+
15+const (
16+ DBDir = "tmp/data"
17+ DBFilename = "sqlite.db"
18+)
19+
20+func (a *App) GetDiskDBFilePath() string {
21+ DiskDBfilepath := filepath.Join(a.DBDir, a.DBFilename)
22+ return DiskDBfilepath
23+}
24+
25+// InitializeDB initializes the database
26+func InitializeDB(a *App) (*sql.DB, error) {
27+ if err := a.ensureDBDir(); err != nil {
28+ return nil, fmt.Errorf("failed to ensure DB directory: %w", err)
29+ }
30+
31+ dbPath := a.GetDiskDBFilePath()
32+
33+ dbExists, err := a.checkDBFile(dbPath)
34+ if err != nil {
35+ return nil, fmt.Errorf("failed to check DB file: %w", err)
36+ }
37+
38+ if !dbExists {
39+ if err := bootstrapDB(dbPath); err != nil {
40+ return nil, fmt.Errorf("failed to bootstrap DB: %w", err)
41+ }
42+ }
43+
44+ memDb, err := loadDBToMemory(dbPath)
45+ if err != nil {
46+ return nil, fmt.Errorf("failed to load DB to memory: %w", err)
47+ }
48+
49+ return memDb, nil
50+}
51+
52+// ensureDBDir ensures that the database directory exists.
53+func (a *App) ensureDBDir() error {
54+ if _, err := os.Stat(a.DBDir); os.IsNotExist(err) {
55+ if err := os.MkdirAll(a.DBDir, 0755); err != nil {
56+ return err
57+ }
58+ }
59+ return nil
60+}
61+
62+// checkDBFile checks if the database file exists.
63+func (a *App) checkDBFile(dbPath string) (bool, error) {
64+ if _, err := os.Stat(dbPath); os.IsNotExist(err) {
65+ return false, nil
66+ } else if err != nil {
67+ return false, err
68+ }
69+ return true, nil
70+}
71+
72+// bootstrapDB executes the SQL statements to create the database tables.
73+func bootstrapDB(dbPath string) error {
74+ db, err := sql.Open("sqlite3", dbPath)
75+ if err != nil {
76+ return err
77+ }
78+ defer db.Close()
79+
80+ if err := migrate.CreateUserTable(db); err != nil {
81+ return err
82+ }
83+
84+ return nil
85+}
86+
87+// loadDBToMemory loads the database from disk to memory.
88+func loadDBToMemory(dbPath string) (*sql.DB, error) {
89+ memDb, err := sql.Open("sqlite3", ":memory:")
90+ if err != nil {
91+ return nil, err
92+ }
93+
94+ _, err = memDb.Exec("ATTACH DATABASE '" + dbPath + "' AS diskdb")
95+ if err != nil {
96+ return nil, err
97+ }
98+
99+ _, err = memDb.Exec("CREATE TABLE users AS SELECT * FROM diskdb.users")
100+ if err != nil {
101+ return nil, err
102+ }
103+
104+ _, err = memDb.Exec("DETACH DATABASE diskdb")
105+ if err != nil {
106+ return nil, err
107+ }
108+
109+ return memDb, nil
110+}
111+
112+// GenerateDBChecksum generates a checksum of the database.
113+func GenerateDBChecksum(db *sql.DB) (string, error) {
114+ tables, err := GetTableNames(db)
115+ if err != nil {
116+ return "", err
117+ }
118+
119+ var finalChecksum []byte
120+
121+ for _, table := range tables {
122+ hasher := md5.New() // Create a new hasher for each table
123+
124+ rows, err := db.Query("SELECT * FROM " + table)
125+ if err != nil {
126+ return "", err
127+ }
128+
129+ for rows.Next() {
130+ cols, err := rows.Columns()
131+ if err != nil {
132+ return "", err
133+ }
134+
135+ vals := make([]interface{}, len(cols))
136+ for i := range cols {
137+ vals[i] = new(sql.RawBytes)
138+ }
139+
140+ err = rows.Scan(vals...)
141+ if err != nil {
142+ return "", err
143+ }
144+
145+ for i := range vals {
146+ hasher.Write(*vals[i].(*sql.RawBytes))
147+ }
148+ }
149+
150+ if err := rows.Err(); err != nil {
151+ rows.Close()
152+ return "", err
153+ }
154+
155+ rows.Close()
156+
157+ finalChecksum = append(finalChecksum, hasher.Sum(nil)...)
158+ }
159+
160+ return hex.EncodeToString(finalChecksum), nil
161+}
@@ -0,0 +1,161 @@
1+package app
2+
3+import (
4+ "crypto/md5"
5+ "database/sql"
6+ "encoding/hex"
7+ "fmt"
8+ "os"
9+ "path/filepath"
10+
11+ "github.com/bnema/gordon/internal/database/migrate"
12+ _ "github.com/mattn/go-sqlite3"
13+)
14+
15+const (
16+ DBDir = "tmp/data"
17+ DBFilename = "sqlite.db"
18+)
19+
20+func (a *App) GetDiskDBFilePath() string {
21+ DiskDBfilepath := filepath.Join(a.DBDir, a.DBFilename)
22+ return DiskDBfilepath
23+}
24+
25+// InitializeDB initializes the database
26+func InitializeDB(a *App) (*sql.DB, error) {
27+ if err := a.ensureDBDir(); err != nil {
28+ return nil, fmt.Errorf("failed to ensure DB directory: %w", err)
29+ }
30+
31+ dbPath := a.GetDiskDBFilePath()
32+
33+ dbExists, err := a.checkDBFile(dbPath)
34+ if err != nil {
35+ return nil, fmt.Errorf("failed to check DB file: %w", err)
36+ }
37+
38+ if !dbExists {
39+ if err := bootstrapDB(dbPath); err != nil {
40+ return nil, fmt.Errorf("failed to bootstrap DB: %w", err)
41+ }
42+ }
43+
44+ memDb, err := loadDBToMemory(dbPath)
45+ if err != nil {
46+ return nil, fmt.Errorf("failed to load DB to memory: %w", err)
47+ }
48+
49+ return memDb, nil
50+}
51+
52+// ensureDBDir ensures that the database directory exists.
53+func (a *App) ensureDBDir() error {
54+ if _, err := os.Stat(a.DBDir); os.IsNotExist(err) {
55+ if err := os.MkdirAll(a.DBDir, 0755); err != nil {
56+ return err
57+ }
58+ }
59+ return nil
60+}
61+
62+// checkDBFile checks if the database file exists.
63+func (a *App) checkDBFile(dbPath string) (bool, error) {
64+ if _, err := os.Stat(dbPath); os.IsNotExist(err) {
65+ return false, nil
66+ } else if err != nil {
67+ return false, err
68+ }
69+ return true, nil
70+}
71+
72+// bootstrapDB executes the SQL statements to create the database tables.
73+func bootstrapDB(dbPath string) error {
74+ db, err := sql.Open("sqlite3", dbPath)
75+ if err != nil {
76+ return err
77+ }
78+ defer db.Close()
79+
80+ if err := migrate.CreateUserTable(db); err != nil {
81+ return err
82+ }
83+
84+ return nil
85+}
86+
87+// loadDBToMemory loads the database from disk to memory.
88+func loadDBToMemory(dbPath string) (*sql.DB, error) {
89+ memDb, err := sql.Open("sqlite3", ":memory:")
90+ if err != nil {
91+ return nil, err
92+ }
93+
94+ _, err = memDb.Exec("ATTACH DATABASE '" + dbPath + "' AS diskdb")
95+ if err != nil {
96+ return nil, err
97+ }
98+
99+ _, err = memDb.Exec("CREATE TABLE users AS SELECT * FROM diskdb.users")
100+ if err != nil {
101+ return nil, err
102+ }
103+
104+ _, err = memDb.Exec("DETACH DATABASE diskdb")
105+ if err != nil {
106+ return nil, err
107+ }
108+
109+ return memDb, nil
110+}
111+
112+// GenerateDBChecksum generates a checksum of the database.
113+func GenerateDBChecksum(db *sql.DB) (string, error) {
114+ tables, err := GetTableNames(db)
115+ if err != nil {
116+ return "", err
117+ }
118+
119+ var finalChecksum []byte
120+
121+ for _, table := range tables {
122+ hasher := md5.New() // Create a new hasher for each table
123+
124+ rows, err := db.Query("SELECT * FROM " + table)
125+ if err != nil {
126+ return "", err
127+ }
128+
129+ for rows.Next() {
130+ cols, err := rows.Columns()
131+ if err != nil {
132+ return "", err
133+ }
134+
135+ vals := make([]interface{}, len(cols))
136+ for i := range cols {
137+ vals[i] = new(sql.RawBytes)
138+ }
139+
140+ err = rows.Scan(vals...)
141+ if err != nil {
142+ return "", err
143+ }
144+
145+ for i := range vals {
146+ hasher.Write(*vals[i].(*sql.RawBytes))
147+ }
148+ }
149+
150+ if err := rows.Err(); err != nil {
151+ rows.Close()
152+ return "", err
153+ }
154+
155+ rows.Close()
156+
157+ finalChecksum = append(finalChecksum, hasher.Sum(nil)...)
158+ }
159+
160+ return hex.EncodeToString(finalChecksum), nil
161+}
M internal/database/migrate/bootstrapdb.go +1 -1
@@ -8,7 +8,7 @@ import (
88 // createUserTable creates the 'user' table in the provided SQLite database.
99 func CreateUserTable(db *sql.DB) error {
1010 createTableSQL := `
11- CREATE TABLE user (
11+ CREATE TABLE users (
1212 id INTEGER PRIMARY KEY AUTOINCREMENT,
1313 username TEXT NOT NULL,
1414 password TEXT NOT NULL, -- This should be hashed and salted in a real-world scenario.
@@ -8,7 +8,7 @@ import (
8 // createUserTable creates the 'user' table in the provided SQLite database.8 // createUserTable creates the 'user' table in the provided SQLite database.
9 func CreateUserTable(db *sql.DB) error {9 func CreateUserTable(db *sql.DB) error {
10 createTableSQL := `10 createTableSQL := `
11- CREATE TABLE user (11+ CREATE TABLE users (
12 id INTEGER PRIMARY KEY AUTOINCREMENT,12 id INTEGER PRIMARY KEY AUTOINCREMENT,
13 username TEXT NOT NULL,13 username TEXT NOT NULL,
14 password TEXT NOT NULL, -- This should be hashed and salted in a real-world scenario.14 password TEXT NOT NULL, -- This should be hashed and salted in a real-world scenario.
A internal/database/queries/inspectdb.go +56 -0
@@ -0,0 +1,56 @@
1+package queries
2+
3+import (
4+ "database/sql"
5+ "fmt"
6+)
7+
8+// InspectInMemoryDB inspects the in-memory database. (for debug purpose)
9+func InspectInMemoryDB(memDb *sql.DB) error {
10+ // Query the sqlite_master table to get a list of all tables
11+ rows, err := memDb.Query("SELECT name FROM sqlite_master WHERE type='table'")
12+ fmt.Println("rows", rows)
13+ if err != nil {
14+ return err
15+ }
16+ defer rows.Close()
17+
18+ var tables []string
19+ for rows.Next() {
20+ var tableName string
21+ if err := rows.Scan(&tableName); err != nil {
22+ return err
23+ }
24+ tables = append(tables, tableName)
25+ }
26+
27+ if err := rows.Err(); err != nil {
28+ return err
29+ }
30+
31+ // Print the tables
32+ fmt.Println("Tables in the in-memory database:")
33+ for _, table := range tables {
34+ fmt.Println(table)
35+ }
36+
37+ // If you want to see the content of a specific table (e.g., users):
38+ userRows, err := memDb.Query("SELECT * FROM users")
39+ if err != nil {
40+ return err
41+ }
42+ defer userRows.Close()
43+
44+ fmt.Println("\nContent of the 'users' table:")
45+ for userRows.Next() {
46+ // Assuming users table has columns: id, name
47+ var id int
48+ var name string
49+ if err := userRows.Scan(&id, &name); err != nil {
50+ return err
51+ }
52+ fmt.Printf("ID: %d, Name: %s\n", id, name)
53+ }
54+
55+ return nil
56+}
@@ -0,0 +1,56 @@
1+package queries
2+
3+import (
4+ "database/sql"
5+ "fmt"
6+)
7+
8+// InspectInMemoryDB inspects the in-memory database. (for debug purpose)
9+func InspectInMemoryDB(memDb *sql.DB) error {
10+ // Query the sqlite_master table to get a list of all tables
11+ rows, err := memDb.Query("SELECT name FROM sqlite_master WHERE type='table'")
12+ fmt.Println("rows", rows)
13+ if err != nil {
14+ return err
15+ }
16+ defer rows.Close()
17+
18+ var tables []string
19+ for rows.Next() {
20+ var tableName string
21+ if err := rows.Scan(&tableName); err != nil {
22+ return err
23+ }
24+ tables = append(tables, tableName)
25+ }
26+
27+ if err := rows.Err(); err != nil {
28+ return err
29+ }
30+
31+ // Print the tables
32+ fmt.Println("Tables in the in-memory database:")
33+ for _, table := range tables {
34+ fmt.Println(table)
35+ }
36+
37+ // If you want to see the content of a specific table (e.g., users):
38+ userRows, err := memDb.Query("SELECT * FROM users")
39+ if err != nil {
40+ return err
41+ }
42+ defer userRows.Close()
43+
44+ fmt.Println("\nContent of the 'users' table:")
45+ for userRows.Next() {
46+ // Assuming users table has columns: id, name
47+ var id int
48+ var name string
49+ if err := userRows.Scan(&id, &name); err != nil {
50+ return err
51+ }
52+ fmt.Printf("ID: %d, Name: %s\n", id, name)
53+ }
54+
55+ return nil
56+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close