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 @@ | |||
| 1 | 1 | .env | |
| 2 | 2 | node_modules/ | |
| 3 | + | *tmp/ | |
| 3 | 4 | tmp/* | |
| 4 | 5 | gordon.yml | |
| 5 | 6 | bun.lockb | |
| 6 | 7 | *.log | |
| 7 | - | ||
| @@ -1,7 +1,7 @@ | |||||
| 1 | .env | 1 | .env | ||
| 2 | node_modules/ | 2 | node_modules/ | ||
| 3 | + | *tmp/ | |||
| 3 | tmp/* | 4 | tmp/* | ||
| 4 | gordon.yml | 5 | gordon.yml | ||
| 5 | bun.lockb | 6 | bun.lockb | ||
| 6 | *.log | 7 | *.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 @@ | |||
| 1 | 1 | package main | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "database/sql" | |
| 4 | 5 | "fmt" | |
| 5 | - | "log/slog" | |
| 6 | + | "log" | |
| 7 | + | "os" | |
| 8 | + | "os/signal" | |
| 9 | + | "syscall" | |
| 6 | 10 | ||
| 7 | 11 | "github.com/labstack/echo/v4" | |
| 8 | 12 | ||
| @@ -10,24 +14,42 @@ import ( | |||
| 10 | 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 | 23 | func main() { | |
| 14 | 24 | a := app.NewApp() | |
| 15 | 25 | ||
| 16 | 26 | // Initialize database | |
| 17 | - | db, err := app.InitializeDB(a) | |
| 27 | + | memDb, err := app.InitializeDB(a) | |
| 18 | 28 | if err != nil { | |
| 19 | - | slog.Error("Failed to load database", err) | |
| 20 | - | } else { | |
| 21 | - | slog.Info("Database loaded") | |
| 29 | + | log.Fatal(err) | |
| 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 | 46 | e := echo.New() | |
| 27 | 47 | e.HideBanner = true | |
| 28 | 48 | e.HidePort = true | |
| 29 | 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 | } | |
| @@ -1,8 +1,12 @@ | |||||
| 1 | package main | 1 | 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 database | 26 | // 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 = true | 47 | e.HideBanner = true | ||
| 28 | e.HidePort = true | 48 | 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 @@ | |||
| 1 | 1 | package app | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "database/sql" | |
| 4 | 5 | "io/fs" | |
| 5 | - | "path/filepath" | |
| 6 | 6 | ||
| 7 | 7 | "github.com/bnema/gordon/internal/gotemplate" | |
| 8 | 8 | "github.com/bnema/gordon/internal/webui" | |
| @@ -11,8 +11,6 @@ import ( | |||
| 11 | 11 | const ( | |
| 12 | 12 | BuildVersion = "0.0.2" | |
| 13 | 13 | BuildDir = "tmp" | |
| 14 | - | DBDir = "tmp/data" | |
| 15 | - | DBFilename = "sqlite.db" | |
| 16 | 14 | // Those configs should be read from a config file | |
| 17 | 15 | HttpPort = 1323 | |
| 18 | 16 | TopDomain = "example.com" | |
| @@ -25,20 +23,22 @@ const ( | |||
| 25 | 23 | ) | |
| 26 | 24 | ||
| 27 | 25 | 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 | |
| 42 | 42 | } | |
| 43 | 43 | ||
| 44 | 44 | func NewApp() *App { | |
| @@ -63,6 +63,3 @@ func NewDockerClient() *App { | |||
| 63 | 63 | PodmanSock: PodmanSock, | |
| 64 | 64 | } | |
| 65 | 65 | } | |
| 66 | - | func (a *App) GetDBFilePath() string { | |
| 67 | - | return filepath.Join(a.DBDir, a.DBFilename) | |
| 68 | - | } | |
| @@ -1,8 +1,8 @@ | |||||
| 1 | package app | 1 | 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 file | 14 | // Those configs should be read from a config file | ||
| 17 | HttpPort = 1323 | 15 | 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.FS | 26 | + | TemplateFS fs.FS |
| 29 | - | PublicFS fs.FS | 27 | + | PublicFS fs.FS |
| 30 | - | DB interface{} | 28 | + | BuildVersion string |
| 31 | - | BuildVersion string | 29 | + | BuildDir string |
| 32 | - | BuildDir string | 30 | + | DBDir string |
| 33 | - | DBDir string | 31 | + | DBFilename string |
| 34 | - | DBFilename string | 32 | + | DBPath string |
| 35 | - | TopDomain string | 33 | + | InitialChecksum string |
| 36 | - | SubDomain string | 34 | + | DB *sql.DB |
| 37 | - | AdminPath string | 35 | + | TopDomain string |
| 38 | - | DockerSock string | 36 | + | SubDomain string |
| 39 | - | PodmanEnable bool | 37 | + | AdminPath string |
| 40 | - | PodmanSock string | 38 | + | DockerSock string |
| 41 | - | HttpPort int16 | 39 | + | 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 ( | |||
| 8 | 8 | // createUserTable creates the 'user' table in the provided SQLite database. | |
| 9 | 9 | func CreateUserTable(db *sql.DB) error { | |
| 10 | 10 | createTableSQL := ` | |
| 11 | - | CREATE TABLE user ( | |
| 11 | + | CREATE TABLE users ( | |
| 12 | 12 | id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 13 | 13 | username TEXT NOT NULL, | |
| 14 | 14 | 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 | + | } | |||