Skip to content

Commit

Permalink
feat: add ability to set bookmarks DB location with config (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
JonathanHope authored Nov 13, 2023
1 parent 1ae5bf1 commit c70c19d
Show file tree
Hide file tree
Showing 9 changed files with 362 additions and 123 deletions.
36 changes: 36 additions & 0 deletions cli/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type RootCmd struct {
Remove RemoveCmd `cmd:"" help:"Remove a folder, bookmark, or tag."`
Update UpdateCmd `cmd:"" help:"Update a folder or bookmark."`
List ListCmd `cmd:"" help:"List folders, bookmarks, or tags."`

Config ConfigCmd `cmd:"" help:"Manage the configuration."`
}

// AddCmd is a CLI command to add a bookmark or folder.
Expand Down Expand Up @@ -56,6 +58,16 @@ type AddBookCmd struct {
URL string `arg:"" name:"url" help:"URL of the bookmark."`
}

// ConfigCmd is a CLI command to manage config.
type ConfigCmd struct {
DB DBConfigCmd `cmd:"" help:"Manage the bookmarks database location configuration."`
}

// DBConfigCmd is a CLI command to manage the bookmarks database location config.
type DBConfigCmd struct {
Get GetDBConfigCmd `cmd:"" help:"Get the location of the bookmarks database from the configuration."`
}

// Run add a bookmark.
func (r *AddBookCmd) Run(ctx *Context) error {
start := time.Now()
Expand Down Expand Up @@ -607,6 +619,30 @@ func (r *RemoveTagsCmd) Run(ctx *Context) error {
return nil
}

// GetDBConfigCmd is a CLI command to get the location of the bookmarks database from the config.
type GetDBConfigCmd struct {
}

// Run get the location of the bookmarks database from the config.
func (r *GetDBConfigCmd) Run(ctx *Context) error {
start := time.Now()

dbPath, err := lib.GetDBPathConfig()

if err != nil {
formatError(ctx.Writer, ctx.Formatter, err)
ctx.ReturnCode(1)
return nil
}

elapsed := time.Since(start)

formatConfigResult(ctx.Writer, ctx.Formatter, dbPath)
formatSuccess(ctx.Writer, ctx.Formatter, fmt.Sprintf("Retreived in %s", elapsed))

return nil
}

// RootCmdFactory creates a new RootCmd.
func RootCmdFactory() RootCmd {
var cmd RootCmd
Expand Down
29 changes: 29 additions & 0 deletions cli/cmd/formatters.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,35 @@ func formatTagResults(writer io.Writer, formatter Formatter, tags []string) {
}
}

// formatConfigResult formats a config value.
func formatConfigResult(writer io.Writer, formatter Formatter, value string) {
switch formatter {

case FormatterJSON:
fmt.Fprintf(writer, "\"%s\"\n", value)

case FormatterPretty:
width, _, err := term.GetSize(int(os.Stdin.Fd()))
if err != nil {
panic(err)
}

style := lipgloss.
NewStyle().
Bold(true).
PaddingLeft(1).
PaddingRight(1).
BorderStyle(lipgloss.RoundedBorder()).
MaxWidth(width - 2)

if value == "" {
fmt.Fprintln(writer, style.Render("⚙ N/A"))
} else {
fmt.Fprintln(writer, style.Render(fmt.Sprintf("⚙ %s", value)))
}
}
}

// formatIsFolder formats an is folder value.
func formatIsFolder(isFolder bool) string {
if isFolder {
Expand Down
119 changes: 119 additions & 0 deletions lib/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package lib

import (
"errors"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/knadh/koanf/parsers/toml"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
)

const configFilename = "armaria.toml"
const databaseFilename = "bookmarks.db"

// mkDirAllFn creates a directory if it doesn't already exist.
type mkDirAllFn func(path string, perm os.FileMode) error

// userHomeFn returns the home directory of the current user.
type userHomeFn func() (string, error)

// joinFn joins path segments together.
type joinFn func(elem ...string) string

// GetDBPathConfig gets the path to the bookmarks database from the config file.
func GetDBPathConfig() (string, error) {
config, err := getConfig(runtime.GOOS, os.UserHomeDir, filepath.Join)
if err != nil && !errors.Is(err, ErrConfigMissing) {
return "", err
}

if errors.Is(err, ErrConfigMissing) {
return "", nil
} else {
return config.String("db"), nil
}
}

// getConfig parses the config file.
// If the sentinerl error ErrConfigMissing then it doesn't exist.
func getConfig(goos string, userHome userHomeFn, join joinFn) (*koanf.Koanf, error) {
configPath, err := getConfigPath(goos, userHome, join)
if err != nil {
return nil, err
}

var config = koanf.New(".")
if err := config.Load(file.Provider(configPath), toml.Parser()); err != nil {
if strings.Contains(err.Error(), "no such file or directory") {
return nil, ErrConfigMissing
} else {
return nil, err
}
}

return config, nil
}

// getDatabasePath gets the path to the bookmarks database.
// The path will be (in order of precedence):
// 1) The inputted path
// 2) The path in the config file
// 3) The default path (getFolderPath() + "bookmarks.db")
func getDatabasePath(inputPath NullString, configPath string, goos string, mkDirAll mkDirAllFn, userHome userHomeFn, join joinFn) (string, error) {
if inputPath.Valid && inputPath.Dirty {
return inputPath.String, nil
} else if configPath != "" {
return configPath, nil
} else {
folder, err := getFolderPath(goos, userHome, join)
if err != nil {
return "", err
}

if err = mkDirAll(folder, os.ModePerm); err != nil {
return "", err
}

return join(folder, databaseFilename), nil
}
}

// getConfigPath gets the path to the config file.
// The config file is a TOML file located at getFolderPath() + "bookmarks.db".
func getConfigPath(goos string, userHome userHomeFn, join joinFn) (string, error) {
folder, err := getFolderPath(goos, userHome, join)
if err != nil {
return "", err
}

return join(folder, configFilename), nil
}

// getFolderPath gets the path to the folder the config (and by default) the database are stored.
// The folder is different per platform and maps to the following:
// - Linux: ~/.armaria
// - Windows: ~/AppData/Local/Armaria
// - Mac: ~/Library/Application Support/Armaria
func getFolderPath(goos string, userHome userHomeFn, join joinFn) (string, error) {
home, err := userHome()
if err != nil {
return "", err
}

var folder string
if goos == "linux" {
folder = join(home, ".armaria")
} else if goos == "windows" {
folder = join(home, "AppData", "Local", "Armaria")
} else if goos == "darwin" {
folder = join(home, "Library", "Application Support", "Armaria")
} else {
panic("Unsupported operating system")
}

return folder, nil
}
131 changes: 131 additions & 0 deletions lib/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package lib

import (
"os"
"path"
"testing"
)

func TestGetConfigPath(t *testing.T) {
type test struct {
goos string
configPath string
folderCreated bool
}

tests := []test{
{
goos: "windows",
configPath: "~/AppData/Local/Armaria/armaria.toml",
folderCreated: false,
},
{
goos: "linux",
configPath: "~/.armaria/armaria.toml",
folderCreated: false,
},
{
goos: "darwin",
configPath: "~/Library/Application Support/Armaria/armaria.toml",
folderCreated: false,
},
}

userHome := func() (string, error) {
return "~", nil
}

for _, tc := range tests {
t.Run(tc.goos, func(t *testing.T) {
folderCreated := false

got, err := getConfigPath(tc.goos, userHome, path.Join)
if err != nil {
t.Fatalf("unexpected error: %+v", err)
}

if folderCreated != tc.folderCreated {
t.Fatalf("folder created: got %+v; want %+v", folderCreated, tc.folderCreated)
}

if got != tc.configPath {
t.Errorf("db: got %+v; want %+v", got, tc.configPath)
}
})
}
}

func TestGetDatabasePath(t *testing.T) {
type test struct {
inputPath NullString
configPath string
goos string
folder string
db string
folderCreated bool
}

tests := []test{
{
inputPath: NullStringFromPtr(nil),
configPath: "",
goos: "windows",
folder: "~/AppData/Local/Armaria",
db: "~/AppData/Local/Armaria/bookmarks.db",
folderCreated: true,
},
{
inputPath: NullStringFromPtr(nil),
configPath: "",
goos: "linux",
folder: "~/.armaria",
db: "~/.armaria/bookmarks.db",
folderCreated: true,
},
{
inputPath: NullStringFromPtr(nil),
configPath: "",
goos: "darwin",
folder: "~/Library/Application Support/Armaria",
db: "~/Library/Application Support/Armaria/bookmarks.db",
folderCreated: true,
},
{
inputPath: NullStringFrom("bookmarks.db"),
configPath: "",
db: "bookmarks.db",
folderCreated: false,
},
}

userHome := func() (string, error) {
return "~", nil
}

for _, tc := range tests {
t.Run(tc.goos, func(t *testing.T) {
folderCreated := false
mkDirAll := func(path string, perm os.FileMode) error {
folderCreated = true
if path != tc.folder {
t.Errorf("folder: got %+v; want %+v", path, tc.folder)
}

return nil
}

got, err := getDatabasePath(tc.inputPath, tc.configPath, tc.goos, mkDirAll, userHome, path.Join)
if err != nil {
t.Fatalf("unexpected error: %+v", err)
}

if folderCreated != tc.folderCreated {
t.Fatalf("folder created: got %+v; want %+v", folderCreated, tc.folderCreated)
}

if got != tc.db {
t.Errorf("db: got %+v; want %+v", got, tc.db)
}
})
}
}
2 changes: 2 additions & 0 deletions lib/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,6 @@ var (
ErrInvalidDirection = errors.New("invalid direction")
// ErrQueryTooShort is returned when a provided query is too short.
ErrQueryTooShort = errors.New("query too short")
// ErrConfigMissing is returned when the config file is missing.
ErrConfigMissing = errors.New("config is missing")
)
10 changes: 10 additions & 0 deletions lib/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,24 @@ go 1.20
require (
github.com/blockloop/scan/v2 v2.5.0
github.com/google/uuid v1.4.0
github.com/knadh/koanf/providers/file v0.1.0
github.com/knadh/koanf/v2 v2.0.1
github.com/mattn/go-sqlite3 v1.14.18
github.com/nullism/bqb v1.7.1
github.com/pressly/goose/v3 v3.15.1
github.com/samber/lo v1.38.1
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0
github.com/knadh/koanf/parsers/toml v0.1.0
)

require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.13.0 // indirect
)
Loading

0 comments on commit c70c19d

Please sign in to comment.