Skip to content

Commit

Permalink
cmd/worklog: add initial generalised database wiring
Browse files Browse the repository at this point in the history
Deprecate the database_dir configuration, to be removed in a month.
  • Loading branch information
kortschak committed Sep 21, 2024
1 parent c7ae2e5 commit 9971453
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 12 deletions.
2 changes: 1 addition & 1 deletion cmd/worklog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ log_mode = "log"
log_level = "info"
[module.worklog.options]
database_dir = "worklog"
database = "sqlite:worklog"
[module.worklog.options.rules.afk]
name = "afk-watcher"
Expand Down
19 changes: 13 additions & 6 deletions cmd/worklog/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,19 @@ type Config struct {
LogLevel *slog.Level `json:"log_level,omitempty"`
AddSource *bool `json:"log_add_source,omitempty"`
Options struct {
DynamicLocation *bool `json:"dynamic_location,omitempty"`
Web *Web `json:"web,omitempty"`
DatabaseDir string `json:"database_dir,omitempty"` // Relative to XDG_STATE_HOME.
Hostname string `json:"hostname,omitempty"`
Heartbeat *rpc.Duration `json:"heartbeat,omitempty"`
Rules map[string]Rule `json:"rules,omitempty"`
DynamicLocation *bool `json:"dynamic_location,omitempty"`
Web *Web `json:"web,omitempty"`
// Database is the URL location of the worklog
// database. When the scheme is sqlite, the location
// is a directory relative to XDG_STATE_HOME as
// URL opaque data.
Database string `json:"database,omitempty"`
Hostname string `json:"hostname,omitempty"`
Heartbeat *rpc.Duration `json:"heartbeat,omitempty"`
Rules map[string]Rule `json:"rules,omitempty"`

// Deprecated: Use Database with sqlite scheme.
DatabaseDir string `json:"database_dir,omitempty"` // Relative to XDG_STATE_HOME.
} `json:"options,omitempty"`
}

Expand Down
46 changes: 43 additions & 3 deletions cmd/worklog/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,20 @@ func (d *daemon) Handle(ctx context.Context, req *jsonrpc2.Request) (any, error)
}
}

if m.Body.Options.DatabaseDir != "" {
dir, err := xdg.State(m.Body.Options.DatabaseDir)
databaseDir, err := dbDir(m.Body)
if err != nil {
d.log.LogAttrs(ctx, slog.LevelError, "configure database", slog.Any("error", err))
return nil, rpc.NewError(rpc.ErrCodeInvalidMessage,
err.Error(),
map[string]any{
"type": rpc.ErrCodeParameters,
"database": m.Body.Options.Database,
"database_dir": m.Body.Options.DatabaseDir,
},
)
}
if databaseDir != "" {
dir, err := xdg.State(databaseDir)
switch err {
case nil:
case syscall.ENOENT:
Expand All @@ -330,7 +342,7 @@ func (d *daemon) Handle(ctx context.Context, req *jsonrpc2.Request) (any, error)
d.log.LogAttrs(ctx, slog.LevelError, "configure database", slog.String("error", "no XDG_STATE_HOME"))
return nil, err
}
dir = filepath.Join(dir, m.Body.Options.DatabaseDir)
dir = filepath.Join(dir, databaseDir)
err = os.Mkdir(dir, 0o750)
if err != nil {
err := err.(*os.PathError) // See godoc for os.Mkdir for why this is safe.
Expand Down Expand Up @@ -392,6 +404,34 @@ func (d *daemon) Handle(ctx context.Context, req *jsonrpc2.Request) (any, error)
}
}

func dbDir(cfg worklog.Config) (string, error) {
opt := cfg.Options
if opt.Database == "" {
return opt.DatabaseDir, nil
}
u, err := url.Parse(opt.Database)
if err != nil {
return "", err
}
switch u.Scheme {
case "":
return "", errors.New("missing scheme in database configuration")
case "sqlite":
if opt.DatabaseDir != "" && u.Opaque != opt.DatabaseDir {
return "", fmt.Errorf("inconsistent database directory configuration: (%s:)%s != %s", u.Scheme, u.Opaque, opt.DatabaseDir)
}
if u.Opaque == "" {
return "", fmt.Errorf("sqlite configuration missing opaque data: %s", opt.Database)
}
return u.Opaque, nil
default:
if opt.DatabaseDir != "" {
return "", fmt.Errorf("inconsistent database configuration: both %s database and sqlite directory configured", u.Scheme)
}
return "", nil
}
}

func (d *daemon) replaceTimezone(ctx context.Context, dynamic *bool) {
if dynamic == nil {
return
Expand Down
88 changes: 87 additions & 1 deletion cmd/worklog/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,11 @@ func TestDaemon(t *testing.T) {
type options struct {
DynamicLocation *bool `json:"dynamic_location,omitempty"`
Web *worklog.Web `json:"web,omitempty"`
DatabaseDir string `json:"database_dir,omitempty"` // Relative to XDG_STATE_HOME.
Database string `json:"database,omitempty"`
Hostname string `json:"hostname,omitempty"`
Heartbeat *rpc.Duration `json:"heartbeat,omitempty"`
Rules map[string]worklog.Rule `json:"rules,omitempty"`
DatabaseDir string `json:"database_dir,omitempty"` // Relative to XDG_STATE_HOME.
}
err := conn.Call(ctx, "configure", rpc.NewMessage(uid, worklog.Config{
Options: options{
Expand Down Expand Up @@ -1715,3 +1716,88 @@ func TestMergeReplacements(t *testing.T) {
})
}
}

var dbDirTests = []struct {
name string
config worklog.Config
want string
wantErr error
}{
{
name: "none",
},
{
name: "deprecated",
config: mkDBDirOptions("database_directory", ""),
want: "database_directory",
},
{
name: "url_only_sqlite",
config: mkDBDirOptions("", "sqlite:database_directory"),
want: "database_directory",
},
{
name: "url_only_postgres",
config: mkDBDirOptions("", "postgres://username:password@localhost:5432/database_name"),
want: "",
},
{
name: "both_consistent",
config: mkDBDirOptions("database_directory", "sqlite:database_directory"),
want: "database_directory",
},
{
name: "missing_scheme",
config: mkDBDirOptions("database_dir", "database_directory"),
want: "",
wantErr: errors.New("missing scheme in database configuration"),
},
{
name: "both_inconsistent_sqlite",
config: mkDBDirOptions("database_dir", "sqlite:database_directory"),
want: "",
wantErr: errors.New("inconsistent database directory configuration: (sqlite:)database_directory != database_dir"),
},
{
name: "invalid_sqlite_url",
config: mkDBDirOptions("", "sqlite:/database_directory"),
want: "",
wantErr: errors.New("sqlite configuration missing opaque data: sqlite:/database_directory"),
},
{
name: "both_inconsistent_postgres",
config: mkDBDirOptions("database_dir", "postgres://username:password@localhost:5432/database_name"),
want: "",
wantErr: errors.New("inconsistent database configuration: both postgres database and sqlite directory configured"),
},
}

func mkDBDirOptions(dir, url string) worklog.Config {
var cfg worklog.Config
cfg.Options.DatabaseDir = dir
cfg.Options.Database = url
return cfg
}

func TestDBDir(t *testing.T) {
for _, test := range dbDirTests {
t.Run(test.name, func(t *testing.T) {
got, err := dbDir(test.config)
if !sameError(err, test.wantErr) {
t.Errorf("unexpected error calling dbDir: got:%v want:%v", err, test.wantErr)
}
if got != test.want {
t.Errorf("unexpected result: got:%q want:%q", got, test.want)
}
})
}
}

func sameError(a, b error) bool {
switch {
case a != nil && b != nil:
return a.Error() == b.Error()
default:
return a == b
}
}
2 changes: 1 addition & 1 deletion testdata/worklog_load.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ log_level = "debug"
log_add_source = true

[module.worklog.options]
database_dir = "worklog"
database = "sqlite:worklog"
hostname = "localhost"
[module.worklog.options.web]
addr = "localhost:7979"
Expand Down
143 changes: 143 additions & 0 deletions testdata/worklog_load_deprecated.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# The build of worklog takes a fair while due to the size of the dependency
# tree, the size of some of the individual dependencies and the absence of
# cachine when building within a test script.
[short] stop 'Skipping long test.'

env HOME=${WORK}

[linux] env XDG_CONFIG_HOME=${HOME}/.config
[linux] env XDG_RUNTIME_DIR=${HOME}/runtime
[linux] mkdir ${XDG_CONFIG_HOME}/dex
[linux] mkdir ${XDG_RUNTIME_DIR}
[linux] mv config.toml ${HOME}/.config/dex/config.toml

[darwin] mkdir ${HOME}'/Library/Application Support/dex'
[darwin] mv config.toml ${HOME}'/Library/Application Support/dex/config.toml'

env GOBIN=${WORK}/bin
env PATH=${GOBIN}:${PATH}
cd ${PKG_ROOT}
go install ./cmd/worklog
cd ${WORK}

dex -log debug -lines &dex&
sleep 1s

POST dump.json http://localhost:7979/load/?replace=true

GET -json http://localhost:7979/dump/
cmp stdout want.json

GET http://localhost:7979/query?sql=select+count(*)+as+event_count+from+events
cmp stdout want_event_count.json

-- config.toml --
[kernel]
device = []
network = "tcp"

[module.worklog]
path = "worklog"
log_mode = "log"
log_level = "debug"
log_add_source = true

[module.worklog.options]
database_dir = "worklog"
hostname = "localhost"
[module.worklog.options.web]
addr = "localhost:7979"
can_modify = true

-- dump.json --
{
"buckets": [
{
"id": "afk_localhost",
"name": "afk-watcher",
"type": "afkstatus",
"client": "worklog",
"hostname": "localhost",
"created": "2023-12-04T17:21:27.424207553+10:30",
"events": [
{
"bucket": "afk",
"id": 2,
"start": "2023-12-04T17:21:28.270750821+10:30",
"end": "2023-12-04T17:21:28.270750821+10:30",
"data": {
"afk": false,
"locked": false
}
}
]
},
{
"id": "window_localhost",
"name": "window-watcher",
"type": "currentwindow",
"client": "worklog",
"hostname": "localhost",
"created": "2023-12-04T17:21:27.428793055+10:30",
"events": [
{
"bucket": "window",
"id": 1,
"start": "2023-12-04T17:21:28.270750821+10:30",
"end": "2023-12-04T17:21:28.270750821+10:30",
"data": {
"app": "Gnome-terminal",
"title": "Terminal"
}
}
]
}
]
}
-- want.json --
{
"buckets": [
{
"id": "afk_localhost",
"name": "afk-watcher",
"type": "afkstatus",
"client": "worklog",
"hostname": "localhost",
"created": "2023-12-04T17:21:27.424207553+10:30",
"events": [
{
"bucket": "afk",
"id": 1,
"start": "2023-12-04T17:21:28.270750821+10:30",
"end": "2023-12-04T17:21:28.270750821+10:30",
"data": {
"afk": false,
"locked": false
}
}
]
},
{
"id": "window_localhost",
"name": "window-watcher",
"type": "currentwindow",
"client": "worklog",
"hostname": "localhost",
"created": "2023-12-04T17:21:27.428793055+10:30",
"events": [
{
"bucket": "window",
"id": 2,
"start": "2023-12-04T17:21:28.270750821+10:30",
"end": "2023-12-04T17:21:28.270750821+10:30",
"data": {
"app": "Gnome-terminal",
"title": "Terminal"
}
}
]
}
]
}
-- want_event_count.json --
[{"event_count":2}]

0 comments on commit 9971453

Please sign in to comment.