all repos

smutok @ e6747ea

yet another tui rss reader (not abandoned, just paused development)

smutok/internal/config/config.go(view raw)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package config

import (
	_ "embed"
	"errors"
	"os"
	"path/filepath"
	"strings"

	"github.com/adrg/xdg"
	"github.com/pelletier/go-toml/v2"
	"olexsmir.xyz/x/envy"
)

//go:embed config.toml
var defaultConfig []byte

var appName = envy.GetOrDefault("APPNAME", "smutok")

var (
	ErrUnsetPasswordEnv     = errors.New("password env is unset")
	ErrNotInitializedConfig = errors.New("config is not initialized")
	ErrConfigAlreadyExists  = errors.New("config already exists")
	ErrPasswordFileNotFound = errors.New("password file not found")
	ErrEmptyPasswordFile    = errors.New("password file is empty")
)

type Config struct {
	DBPath      string
	LogFilePath string
	FreshRSS    struct {
		Host     string `toml:"host"`
		Username string `toml:"username"`
		Password string `toml:"password"`
	} `toml:"freshrss"`
}

func New() (*Config, error) {
	configPath := MustGetConfigFilePath()
	if !isFileExists(configPath) {
		return nil, ErrNotInitializedConfig
	}

	configRaw, err := os.ReadFile(configPath)
	if err != nil {
		return nil, err
	}

	var config *Config
	if cerr := toml.Unmarshal(configRaw, &config); cerr != nil {
		return nil, cerr
	}

	passwd, err := parsePassword(
		config.FreshRSS.Password,
		filepath.Dir(configPath))
	if err != nil {
		return nil, err
	}

	config.FreshRSS.Password = passwd
	config.DBPath = mustGetStateFile("smutok.sqlite")
	config.LogFilePath = mustGetStateFile("smutok.log")

	return config, nil
}

func Init() error {
	configPath := MustGetConfigFilePath()
	if isFileExists(configPath) {
		return ErrConfigAlreadyExists
	}

	err := os.WriteFile(configPath, defaultConfig, 0o644)
	return err
}

func MustGetConfigFilePath() string { return mustGetConfigFile("config.toml") }

func mustGetStateFile(file string) string {
	stateFile, err := xdg.StateFile(filepath.Join(appName, file))
	if err != nil {
		panic(err)
	}
	return stateFile
}

func mustGetConfigFile(file string) string {
	configFile, err := xdg.ConfigFile(filepath.Join(appName, file))
	if err != nil {
		panic(err)
	}
	return configFile
}

func parsePassword(passwd string, baseDir string) (string, error) {
	envPrefix := "$env:"
	filePrefix := "file:"

	switch {
	case strings.HasPrefix(passwd, envPrefix):
		env := os.Getenv(passwd[len(envPrefix):])
		if env == "" {
			return "", ErrUnsetPasswordEnv
		}
		return env, nil

	case strings.HasPrefix(passwd, filePrefix):
		fpath := os.ExpandEnv(passwd[len(filePrefix):])

		if strings.HasPrefix(fpath, "./") {
			fpath = filepath.Join(baseDir, fpath)
		}

		if !isFileExists(fpath) {
			return "", ErrPasswordFileNotFound
		}

		data, err := os.ReadFile(fpath)
		if err != nil {
			return "", err
		}

		password := strings.TrimSpace(string(data))
		if password == "" {
			return "", ErrEmptyPasswordFile
		}
		return password, nil

	default:
		return passwd, nil
	}
}

func isFileExists(fpath string) bool {
	_, err := os.Stat(fpath)
	return err == nil
}