The very basic client app, not adapted for mobiles.
This commit is contained in:
116
client/internal/services/core/database/database.go
Normal file
116
client/internal/services/core/database/database.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
|
||||
"bunker/client/internal/application"
|
||||
"bunker/client/internal/services/core"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
// sqlite adapter.
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
var _ = core.Database(&database{})
|
||||
|
||||
type database struct {
|
||||
mainWindow core.MainWindow
|
||||
options core.Options
|
||||
app *application.Application
|
||||
db *sqlx.DB
|
||||
migrations map[string]fs.FS
|
||||
dbPath string
|
||||
version int64
|
||||
}
|
||||
|
||||
// Initialize initializes service.
|
||||
func Initialize(app *application.Application) error {
|
||||
db := &database{
|
||||
app: app,
|
||||
}
|
||||
|
||||
if err := app.RegisterService(db); err != nil {
|
||||
return fmt.Errorf("%w: %w", core.ErrMainWindow, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) Configure() error {
|
||||
if err := d.configureDBPath(); err != nil {
|
||||
return fmt.Errorf("configure: %w", err)
|
||||
}
|
||||
|
||||
db, err := sqlx.Open("sqlite", d.dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("configure: open database: %w", err)
|
||||
}
|
||||
|
||||
d.db = db
|
||||
|
||||
slog.Info("Database opened.", "path", d.dbPath)
|
||||
|
||||
if err := d.initializeSysInfoHandler(); err != nil {
|
||||
return fmt.Errorf("configure: %w", err)
|
||||
}
|
||||
|
||||
if err := d.initializeOptions(); err != nil {
|
||||
return fmt.Errorf("configure: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) ConnectDependencies() error {
|
||||
mainWindowRaw := d.app.Service(core.ServiceNameMainWindow)
|
||||
if mainWindowRaw == nil {
|
||||
return fmt.Errorf("connect dependencies: get main window: %w", application.ErrServiceNotFound)
|
||||
}
|
||||
|
||||
mainWindow, valid := mainWindowRaw.(core.MainWindow)
|
||||
if !valid {
|
||||
return fmt.Errorf("connect dependencies: type assert main window: %w", core.ErrMainWindowIsInvalid)
|
||||
}
|
||||
|
||||
d.mainWindow = mainWindow
|
||||
|
||||
optionsRaw := d.app.Service(core.ServiceNameOptions)
|
||||
if optionsRaw == nil {
|
||||
return fmt.Errorf("connect dependencies: get options: %w", application.ErrServiceNotFound)
|
||||
}
|
||||
|
||||
options, valid := optionsRaw.(core.Options)
|
||||
if !valid {
|
||||
return fmt.Errorf("connect dependencies: type assert options: %w", core.ErrOptionsIsInvalid)
|
||||
}
|
||||
|
||||
d.options = options
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) Initialize() error {
|
||||
d.migrations = make(map[string]fs.FS, 0)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) Name() string {
|
||||
return core.ServiceNameDatabase
|
||||
}
|
||||
|
||||
func (d *database) LaunchStartupTasks() error {
|
||||
// Запускаем миграции.
|
||||
if err := d.applyMigrations(); err != nil {
|
||||
return fmt.Errorf("launch startup tasks: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) Shutdown() error {
|
||||
return nil
|
||||
}
|
30
client/internal/services/core/database/dbpath.go
Normal file
30
client/internal/services/core/database/dbpath.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"bunker/commons"
|
||||
)
|
||||
|
||||
func (d *database) configureDBPath() error {
|
||||
rootDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("configure DB path: get config dir: %w", err)
|
||||
}
|
||||
|
||||
rootDir = filepath.Join(rootDir, commons.ClientAppID)
|
||||
|
||||
//nolint:mnd
|
||||
if err := os.MkdirAll(rootDir, 0o700); err != nil {
|
||||
return fmt.Errorf("configure DB path: create dir: %w", err)
|
||||
}
|
||||
|
||||
d.dbPath = filepath.Join(rootDir, "database.sqlite3")
|
||||
|
||||
slog.Info("Database path configured.", "path", d.dbPath)
|
||||
|
||||
return nil
|
||||
}
|
78
client/internal/services/core/database/migrations.go
Normal file
78
client/internal/services/core/database/migrations.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"bunker/client/internal/services/core"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
var errMigrationsAlreadyRegistered = errors.New("migrations already registered")
|
||||
|
||||
func (d *database) applyMigrations() error {
|
||||
slog.Info("Migrating database...")
|
||||
|
||||
modules := make([]string, 0)
|
||||
|
||||
for module := range d.migrations {
|
||||
modules = append(modules, module)
|
||||
}
|
||||
|
||||
sort.Strings(modules)
|
||||
|
||||
_ = goose.SetDialect(string(goose.DialectSQLite3))
|
||||
|
||||
for _, module := range modules {
|
||||
slog.Info("Migrating database for module...", "module", module)
|
||||
|
||||
goose.SetBaseFS(d.migrations[module])
|
||||
goose.SetTableName(strings.ReplaceAll(module, "/", "_") + "_migrations")
|
||||
|
||||
if err := goose.Up(d.db.DB, "migrations"); err != nil {
|
||||
return fmt.Errorf("%w: applying migrations for module '%s': %w", core.ErrDatabase, module, err)
|
||||
}
|
||||
|
||||
moduleDBVersion, err := goose.GetDBVersion(d.db.DB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: get database version for module '%s': %w", core.ErrDatabase, module, err)
|
||||
}
|
||||
|
||||
d.version += moduleDBVersion
|
||||
|
||||
slog.Info(
|
||||
"Database for module migrated to latest version",
|
||||
"module", module,
|
||||
"module_db_version", moduleDBVersion,
|
||||
"db_version", d.version,
|
||||
)
|
||||
}
|
||||
|
||||
slog.Info("Database migrated.", "version", d.version)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) RegisterMigrations(moduleName string, fSys fs.FS) error {
|
||||
slog.Debug("Registering migrations for service.", "service", moduleName)
|
||||
|
||||
if _, found := d.migrations[moduleName]; found {
|
||||
return fmt.Errorf(
|
||||
"%w: RegisterMigrations: module '%s': %w",
|
||||
core.ErrDatabase,
|
||||
moduleName,
|
||||
errMigrationsAlreadyRegistered,
|
||||
)
|
||||
}
|
||||
|
||||
d.migrations[moduleName] = fSys
|
||||
|
||||
slog.Debug("Migrations for service successfully registered.", "service", moduleName)
|
||||
|
||||
return nil
|
||||
}
|
36
client/internal/services/core/database/options.go
Normal file
36
client/internal/services/core/database/options.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"bunker/client/internal/services/core/options/dto"
|
||||
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
func (d *database) initializeOptions() error {
|
||||
databaseMaxIdleConnsEntry := widget.NewEntry()
|
||||
databaseMaxIdleConns := widget.NewFormItem("Maximum idle connections", databaseMaxIdleConnsEntry)
|
||||
|
||||
databaseMaxOpenedConnsEntry := widget.NewEntry()
|
||||
databaseMaxOpenedConns := widget.NewFormItem("Maximum opened connections", databaseMaxOpenedConnsEntry)
|
||||
|
||||
optionsWidgetForm := widget.NewForm(
|
||||
databaseMaxIdleConns,
|
||||
databaseMaxOpenedConns,
|
||||
)
|
||||
|
||||
if err := d.options.RegisterOptionsWidget(&dto.OptionPane{
|
||||
Widget: optionsWidgetForm,
|
||||
SaveHandler: d.saveOptions,
|
||||
Name: "Database",
|
||||
}); err != nil {
|
||||
return fmt.Errorf("register options widget: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) saveOptions() error {
|
||||
return nil
|
||||
}
|
80
client/internal/services/core/database/queries.go
Normal file
80
client/internal/services/core/database/queries.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"bunker/client/internal/services/core"
|
||||
)
|
||||
|
||||
// Exec is a proxy for ExecContext from sqlx.
|
||||
func (d *database) Exec(ctx context.Context, query string, params ...interface{}) error {
|
||||
if strings.Contains(query, "?") {
|
||||
query = d.db.Rebind(query)
|
||||
}
|
||||
|
||||
slog.Debug("Executing query.", "query", query, "params", fmt.Sprintf("%+v", params), "module", "core/database")
|
||||
|
||||
if _, err := d.db.ExecContext(ctx, query, params...); err != nil {
|
||||
return fmt.Errorf("%w: failed to Exec(): %w", core.ErrDatabase, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get is a proxy for GetContext from sqlx.
|
||||
func (d *database) Get(ctx context.Context, target interface{}, query string, params ...interface{}) error {
|
||||
if strings.Contains(query, "?") {
|
||||
query = d.db.Rebind(query)
|
||||
}
|
||||
|
||||
slog.Debug(
|
||||
"Getting single data from database with query.",
|
||||
"query", query,
|
||||
"params", fmt.Sprintf("%+v", params),
|
||||
"module", "core/database",
|
||||
)
|
||||
|
||||
if err := d.db.GetContext(ctx, target, query, params...); err != nil {
|
||||
return fmt.Errorf("%w: failed to Get(): %w", core.ErrDatabase, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NamedExec is a proxy for NamedExecContext from sqlx.
|
||||
func (d *database) NamedExec(ctx context.Context, query string, param interface{}) error {
|
||||
if strings.Contains(query, "?") {
|
||||
query = d.db.Rebind(query)
|
||||
}
|
||||
|
||||
slog.Debug("Executing named query.", "query", query, "params", fmt.Sprintf("%+v", param), "module", "core/database")
|
||||
|
||||
if _, err := d.db.NamedExecContext(ctx, query, param); err != nil {
|
||||
return fmt.Errorf("%w: failed to NamedExec(): %w", core.ErrDatabase, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Select is a proxy for SelectContext from sqlx.
|
||||
func (d *database) Select(ctx context.Context, target interface{}, query string, params ...interface{}) error {
|
||||
if strings.Contains(query, "?") {
|
||||
query = d.db.Rebind(query)
|
||||
}
|
||||
|
||||
slog.Debug(
|
||||
"Selecting from database with query.",
|
||||
"query", query,
|
||||
"params", fmt.Sprintf("%+v", params),
|
||||
"module", "core/database",
|
||||
)
|
||||
|
||||
if err := d.db.SelectContext(ctx, target, query, params...); err != nil {
|
||||
return fmt.Errorf("%w: failed to Select(): %w", core.ErrDatabase, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
51
client/internal/services/core/database/sys_info_handler.go
Normal file
51
client/internal/services/core/database/sys_info_handler.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func (d *database) initializeSysInfoHandler() error {
|
||||
if err := d.mainWindow.RegisterAboutWindowSysInfoHandler(
|
||||
"database_data",
|
||||
d.sysInfoHandlerDatabaseVersion,
|
||||
); err != nil {
|
||||
return fmt.Errorf("register database_data handler: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *database) sysInfoHandlerDatabaseVersion() string {
|
||||
var dbFileSize, sizeDivisionsCount int64
|
||||
|
||||
var databaseSizeString string
|
||||
|
||||
fileInfo, err := os.Stat(d.dbPath)
|
||||
if err == nil {
|
||||
dbFileSize = fileInfo.Size()
|
||||
}
|
||||
|
||||
for {
|
||||
//nolint:mnd
|
||||
if dbFileSize > 1024 {
|
||||
dbFileSize /= 1024
|
||||
sizeDivisionsCount++
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
switch sizeDivisionsCount {
|
||||
case 1:
|
||||
databaseSizeString = fmt.Sprintf("- Database file size: %d KB", dbFileSize)
|
||||
case 2: //nolint:mnd
|
||||
databaseSizeString = fmt.Sprintf("- Database file size: %d MB", dbFileSize)
|
||||
default:
|
||||
databaseSizeString = fmt.Sprintf("- Database file size: %d GB", dbFileSize)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("### Database\n\n- Database version: %d\n%s\n", d.version, databaseSizeString)
|
||||
}
|
79
client/internal/services/core/database/transaction.go
Normal file
79
client/internal/services/core/database/transaction.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"bunker/client/internal/services/core"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type transaction struct {
|
||||
transaction *sqlx.Tx
|
||||
}
|
||||
|
||||
func (d *database) Transaction(ctx context.Context) (core.DatabaseTransaction, error) {
|
||||
txn, err := d.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: starting transaction: %w", core.ErrDatabase, err)
|
||||
}
|
||||
|
||||
txHandler := &transaction{
|
||||
transaction: txn,
|
||||
}
|
||||
|
||||
return txHandler, nil
|
||||
}
|
||||
|
||||
func (t *transaction) Apply(steps ...core.TransactionFunc) error {
|
||||
for stepNumber, stepFunc := range steps {
|
||||
if err := stepFunc(t.transaction); err != nil {
|
||||
slog.Error(
|
||||
"Error occurred.",
|
||||
"step", stepNumber,
|
||||
"error", err.Error(),
|
||||
"module", "core/database",
|
||||
"subsystem", "transaction",
|
||||
)
|
||||
|
||||
if rollbackErr := t.transaction.Rollback(); rollbackErr != nil {
|
||||
slog.Error(
|
||||
"Transaction rollback failed.",
|
||||
"error", err.Error(),
|
||||
"module", "core/database",
|
||||
"subsystem", "transaction",
|
||||
)
|
||||
|
||||
return fmt.Errorf("%w: transaction rollback: %w", core.ErrDatabase, rollbackErr)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := t.transaction.Commit(); err != nil {
|
||||
slog.Error(
|
||||
"Transaction commit failed.",
|
||||
"error", err.Error(),
|
||||
"module", "core/database",
|
||||
"subsystem", "transaction",
|
||||
)
|
||||
|
||||
if rollbackErr := t.transaction.Rollback(); rollbackErr != nil {
|
||||
slog.Error(
|
||||
"Transaction rollback failed.",
|
||||
"error", err.Error(),
|
||||
"module", "core/database",
|
||||
"subsystem", "transaction",
|
||||
)
|
||||
|
||||
return fmt.Errorf("%w: transaction rollback: %w", core.ErrDatabase, rollbackErr)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: transaction commit: %w", core.ErrDatabase, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user