diff --git a/cmd/main.go b/cmd/main.go index 0ba97e2..03cfd03 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,7 +3,6 @@ package main import ( "fmt" "log/slog" - _ "net/http/pprof" "os" "github.com/AbdeltwabMF/gomeet/configs" @@ -12,16 +11,17 @@ import ( ) func main() { - file, err := configs.InitLogger() + f, err := configs.OpenLog(os.O_CREATE | os.O_TRUNC | os.O_WRONLY) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } - defer file.Close() + defer f.Close() + configs.InitLogger(f) cfg, err := configs.LoadConfig() if err != nil { - slog.Error(err.Error()) + slog.Error(err.Error(), slog.Any("func", configs.CallerInfo())) os.Exit(1) } diff --git a/configs/configs.go b/configs/configs.go index 92765ac..bfffc32 100644 --- a/configs/configs.go +++ b/configs/configs.go @@ -2,23 +2,28 @@ package configs import ( "encoding/json" + "io" "log/slog" "os" + "path" "path/filepath" + "runtime" "github.com/AbdeltwabMF/gomeet/internal/platform" ) -const LogFile = "log.txt" -const ConfigFile = "config.json" -const CredentialsFile = "credentials.json" -const TokenFile = "token.json" +const ( + LogFile = "log.txt" + ConfigFile = "config.json" + CredentialsFile = "credentials.json" + TokenFile = "token.json" +) type Config struct { AutoStart bool `json:"auto_start"` } -type Start struct { +type start struct { Time string `json:"time"` Days []string `json:"days"` } @@ -26,46 +31,99 @@ type Start struct { type Event struct { Summary string `json:"summary"` Url string `json:"url"` - Start Start `json:"start"` + Start start `json:"start"` } type Events struct { Items []*Event `json:"events"` } -func LoadConfig() (*Config, error) { - c, err := platform.ConfigDir() +type FuncInfo struct { + Name string + File string + Line int +} + +// OpenLog opens the log file with the specified flags. +func OpenLog(flags int) (*os.File, error) { + d, err := platform.LogDir() if err != nil { return nil, err } - file, err := os.OpenFile(filepath.Join(c, ConfigFile), os.O_CREATE|os.O_RDONLY, 0640) + return os.OpenFile(filepath.Join(d, LogFile), flags, 0640) +} + +// OpenConfig opens the configuration file with the specified flags. +func OpenConfig(flags int) (*os.File, error) { + d, err := platform.ConfigDir() if err != nil { return nil, err } - defer file.Close() - var config Config - if err := json.NewDecoder(file).Decode(&config); err != nil { + return os.OpenFile(filepath.Join(d, ConfigFile), flags, 0640) +} + +// OpenCredentials opens the credentials file with the specified flags. +func OpenCredentials(flags int) (*os.File, error) { + d, err := platform.ConfigDir() + if err != nil { return nil, err } - return &config, nil + return os.OpenFile(filepath.Join(d, CredentialsFile), flags, 0600) } -func InitLogger() (*os.File, error) { - lDir, err := platform.LogDir() +// OpenToken opens the token file with the specified flags. +func OpenToken(flags int) (*os.File, error) { + d, err := platform.ConfigDir() if err != nil { return nil, err } - file, err := os.OpenFile(filepath.Join(lDir, LogFile), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0640) + return os.OpenFile(filepath.Join(d, TokenFile), flags, 0600) +} + +// LoadConfig loads the configuration from the configuration file. +func LoadConfig() (*Config, error) { + f, err := OpenConfig(os.O_RDONLY) if err != nil { return nil, err } + defer f.Close() + + cfg := new(Config) + if err := json.NewDecoder(f).Decode(cfg); err != nil { + return nil, err + } + + return cfg, nil +} - logger := slog.New(slog.NewJSONHandler(file, nil)) +// InitLogger initializes the default logger with the provided writer. +func InitLogger(w io.Writer) { + logger := slog.New(slog.NewTextHandler(w, + &slog.HandlerOptions{ + Level: slog.LevelDebug, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + return slog.Any(a.Key, a.Value.Time().Format("2006-01-02 15:04:05")) + } + return a + }}, + )) slog.SetDefault(logger) +} + +// CallerInfo returns information about the caller of the function where it's called. +func CallerInfo() FuncInfo { + pc, file, line, ok := runtime.Caller(1) // 0: Function info, 1: Caller info + if !ok { + return FuncInfo{} + } + + file = path.Base(file) + name := path.Base(runtime.FuncForPC(pc).Name()) - return file, nil + return FuncInfo{Name: name, File: file, Line: line} } diff --git a/internal/googlecal/googlecal.go b/internal/googlecal/googlecal.go index 80b997d..32c479a 100644 --- a/internal/googlecal/googlecal.go +++ b/internal/googlecal/googlecal.go @@ -4,10 +4,10 @@ import ( "context" "encoding/json" "fmt" + "io" "log/slog" "net/http" "os" - "path/filepath" "strings" "time" @@ -20,209 +20,214 @@ import ( "github.com/AbdeltwabMF/gomeet/internal/platform" ) -var CalAttr = slog.String("calendar", "Google") - -func authorizeAccess(cfg *oauth2.Config) (*oauth2.Token, error) { - authzURL := cfg.AuthCodeURL("state-token", oauth2.AccessTypeOffline) +// handleOAuthCallback exchanges the authorization code for a token and sends it to the provided channel. +func handleOAuthCallback(c chan<- *oauth2.Token, cfg *oauth2.Config, w http.ResponseWriter, r *http.Request) { + qv := r.URL.Query() + code := qv.Get("code") + if code == "" { + http.Error(w, "Missing code parameter", http.StatusBadRequest) + return + } - q := strings.Index(authzURL, "?") - err := platform.OpenURL(fmt.Sprintf(`%s"%s"`, authzURL[:q+1], authzURL[q+2:])) + tok, err := cfg.Exchange(context.Background(), code) if err != nil { - return nil, err + http.Error(w, fmt.Sprintf("Unable to retrieve token: %v", err), http.StatusInternalServerError) + return } + fmt.Fprintf(w, "Access granted! You can now close this window.") + + c <- tok +} + +// authorizeAccess opens a browser window for the user to authenticate and authorize access. +func authorizeAccess(cfg *oauth2.Config) error { + c := make(chan *oauth2.Token, 1) - c := make(chan *oauth2.Token, 2) - http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { - handleOAuthCallback(c, w, req, cfg) + server := &http.Server{} + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + handleOAuthCallback(c, cfg, w, r) }) go func() { - if err := http.ListenAndServe("", nil); err != nil { - slog.Error(err.Error()) + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + slog.Error(err.Error(), slog.Any("func", configs.CallerInfo())) } }() - d, err := platform.ConfigDir() - if err != nil { - return nil, err + defer func() { + if err := server.Shutdown(context.Background()); err != nil { + slog.Error(err.Error(), slog.Any("func", configs.CallerInfo())) + } + }() + + url := cfg.AuthCodeURL("state-token", oauth2.AccessTypeOffline) + q := strings.Index(url, "?") + if err := platform.OpenURL(fmt.Sprintf(`%s"%s"`, url[:q+1], url[q+2:])); err != nil { + return err } tok := <-c - return tok, saveToken(filepath.Join(d, configs.TokenFile), tok) + return saveToken(tok) } -func loadToken(path string) (*oauth2.Token, error) { - file, err := os.Open(path) +// saveToken saves the OAuth2 token to a file. +func saveToken(tok *oauth2.Token) error { + f, err := configs.OpenToken(os.O_CREATE | os.O_TRUNC | os.O_WRONLY) if err != nil { - return nil, err + return err } - defer file.Close() + defer f.Close() - tok := &oauth2.Token{} - return tok, json.NewDecoder(file).Decode(tok) + return json.NewEncoder(f).Encode(tok) } -func saveToken(path string, tok *oauth2.Token) error { - file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) +// loadToken loads the OAuth2 token from a file. +func loadToken() (*oauth2.Token, error) { + f, err := configs.OpenToken(os.O_RDONLY) if err != nil { - return err + return nil, err } - defer file.Close() + defer f.Close() - return json.NewEncoder(file).Encode(tok) -} + tok := &oauth2.Token{} + err = json.NewDecoder(f).Decode(tok) -func handleOAuthCallback(c chan<- *oauth2.Token, w http.ResponseWriter, req *http.Request, cfg *oauth2.Config) { - qv := req.URL.Query() - code := qv.Get("code") - if code == "" { - http.Error(w, "Missing code parameter", http.StatusBadRequest) - return - } + return tok, err +} - tok, err := cfg.Exchange(context.Background(), code) +// getToken retrieves the OAuth2 token. If it does not exist, it creates a new token. +func getToken(cfg *oauth2.Config) (*oauth2.Token, error) { + tok, err := loadToken() if err != nil { - http.Error(w, fmt.Sprintf("Unable to retrieve token: %v", err), http.StatusInternalServerError) - return - } else { - fmt.Fprintf(w, "Authentication successful! You can now close this window.") + slog.Error(err.Error(), slog.Any("func", configs.CallerInfo())) + + // Create a new access token + if err := authorizeAccess(cfg); err != nil { + return nil, err + } + + tok, err = loadToken() } - c <- tok + return tok, err } +// initService initializes the Google Calendar API service. func initService() (*calendar.Service, error) { - ctx := context.Background() - - c, err := platform.ConfigDir() + f, err := configs.OpenCredentials(os.O_RDONLY) if err != nil { return nil, err } + defer f.Close() - bytes, err := os.ReadFile(filepath.Join(c, configs.CredentialsFile)) + b, err := io.ReadAll(f) if err != nil { return nil, err } // If modifying these scopes, delete your previously saved token.json - cfg, err := google.ConfigFromJSON(bytes, calendar.CalendarEventsReadonlyScope) + cfg, err := google.ConfigFromJSON(b, calendar.CalendarEventsReadonlyScope) if err != nil { return nil, err } - tok, err := loadToken(filepath.Join(c, configs.TokenFile)) + tok, err := getToken(cfg) if err != nil { - slog.Error(err.Error()) - - tok, err = authorizeAccess(cfg) - if err != nil { - return nil, err - } + return nil, err } client := cfg.Client(context.Background(), tok) - return calendar.NewService(ctx, option.WithHTTPClient(client)) -} + ctx := context.Background() -func waitNextMinute() { - time.Sleep(time.Until(time.Now().Truncate(time.Minute).Add(time.Minute))) + return calendar.NewService(ctx, option.WithHTTPClient(client)) } +// Monitor continuously monitors Google Calendar events. func Monitor(cfg *configs.Config) { - var events *calendar.Events - c := make(chan *calendar.Events, 2) - errc := make(chan error, 2) + srv, err := initService() + if err != nil { + slog.Error(err.Error(), slog.Any("func", configs.CallerInfo())) + return + } + + events := new(calendar.Events) + + fetchTicker := time.NewTicker(time.Minute + time.Second*7) + checkTicker := time.NewTicker(time.Minute) - go Fetch(c, errc) + c := make(chan *calendar.Events, 1) for { select { case events = <-c: - slog.Info("Received events", slog.Int("events.count", len(events.Items)), CalAttr) - case err := <-errc: - slog.Error(fmt.Sprintf("Received error: %v", err), CalAttr) - go func() { - waitNextMinute() - Fetch(c, errc) - }() - default: - if events != nil { - for _, item := range events.Items { - matched, err := Match(item) - if err != nil { - slog.Error(err.Error()) - continue - } - - if matched { - err := Execute(item, cfg.AutoStart) - if err != nil { - slog.Error(fmt.Sprintf("Execute: %v", err.Error()), CalAttr) - } - } - } - } - waitNextMinute() + slog.Info("Received events", slog.Int("count", len(events.Items)), slog.Any("func", configs.CallerInfo())) + case <-fetchTicker.C: + go Fetch(c, srv) + case <-checkTicker.C: + Check(events, cfg) // Check in this goroutine to prevent unsynchronized access to events } } } -// Fetch fetches calendar events and sends them through the provided channel. -// It periodically fetches events, sleeping until the beginning of the next hour between fetches. -func Fetch(ch chan<- *calendar.Events, errch chan<- error) { - srv, err := initService() +// Fetch retrieves upcoming Google Calendar events for the day. +func Fetch(c chan<- *calendar.Events, srv *calendar.Service) { + now := time.Now() + endofday := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC) + + events, err := srv.Events.List("primary"). + TimeMin(now.Format(time.RFC3339)). + TimeMax(endofday.Format(time.RFC3339)). + MaxResults(7). + SingleEvents(true). + OrderBy("startTime"). + Do() if err != nil { - errch <- err + slog.Error(err.Error(), slog.Any("func", configs.CallerInfo())) return } - for { - now := time.Now() - endofday := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC) - events, err := srv.Events.List("primary"). - TimeMin(now.Format(time.RFC3339)). - TimeMax(endofday.Format(time.RFC3339)). - MaxResults(7). - SingleEvents(true). - OrderBy("startTime"). - Do() - - if err != nil { - errch <- err - return - } + c <- events +} - ch <- events - waitNextMinute() +// Check checks all events for a match with the current time and executes actions accordingly. +func Check(events *calendar.Events, cfg *configs.Config) { + for _, e := range events.Items { + if Match(e) { + if err := Execute(e, cfg.AutoStart); err != nil { + slog.Error(err.Error(), slog.Any("func", configs.CallerInfo())) + } + } } } -// Match checks if the given event matches the current time(hh:mm). -func Match(event *calendar.Event) (bool, error) { - now := time.Now() - - t, err := time.Parse(time.RFC3339, event.Start.DateTime) +// Match checks if the calendar event start time matches the current time. +func Match(e *calendar.Event) bool { + st, err := time.Parse(time.RFC3339, e.Start.DateTime) if err != nil { - return false, err + slog.Error(err.Error(), slog.Any("func", configs.CallerInfo())) + return false } - slog.Info("Match", - slog.String("event.time", t.Format("15:04")), - slog.String("now.time", now.Format("15:04")), - CalAttr, + now := time.Now() + slog.Debug("Matching event", + slog.String("now", now.Format("15:04")), + slog.String("then", st.Format("15:04")), + slog.Any("func", configs.CallerInfo()), ) - return t.Format("15:04") == now.Format("15:04"), nil + return st.Format("15:04") == now.Format("15:04") } -// Execute executes actions associated with the given event, such as notifying and potentially starting a meeting. -func Execute(event *calendar.Event, autoStart bool) error { - if err := platform.Notify(event.Summary, event.Location); err != nil { - slog.Error(err.Error()) +// Execute executes actions based on the given calendar event. +func Execute(e *calendar.Event, autoStart bool) error { + if err := platform.Notify(e.Summary, e.Location); err != nil { + slog.Error(err.Error(), slog.Any("func", configs.CallerInfo())) } if autoStart { - return platform.OpenURL(event.Location) + return platform.OpenURL(e.Location) } + return nil } diff --git a/internal/localcal/localcal.go b/internal/localcal/localcal.go index 6d9cbfd..da27e8b 100644 --- a/internal/localcal/localcal.go +++ b/internal/localcal/localcal.go @@ -2,115 +2,92 @@ package localcal import ( "encoding/json" - "fmt" "log/slog" "os" - "path/filepath" "time" "github.com/AbdeltwabMF/gomeet/configs" "github.com/AbdeltwabMF/gomeet/internal/platform" ) -var CalAttr = slog.String("calendar", "Local") - -func waitNextMinute() { - time.Sleep(time.Until(time.Now().Truncate(time.Minute).Add(time.Minute))) -} - +// Monitor continuously monitors local calendar events. func Monitor(cfg *configs.Config) { - var events *configs.Events - c := make(chan *configs.Events, 2) - errc := make(chan error, 2) + events := new(configs.Events) + + loadTicker := time.NewTicker(time.Minute + time.Second*7) + checkTicker := time.NewTicker(time.Minute) - go Load(c, errc) + c := make(chan *configs.Events, 1) for { select { case events = <-c: - slog.Info("Received events", slog.Int("events.count", len(events.Items)), CalAttr) - case err := <-errc: - slog.Error(fmt.Sprintf("Received error: %v", err), CalAttr) - go func() { - waitNextMinute() - Load(c, errc) - }() - default: - if events != nil { - for _, item := range events.Items { - if Match(item) { - err := Execute(item, cfg.AutoStart) - if err != nil { - slog.Error(fmt.Sprintf("Execute: %v", err.Error()), CalAttr) - } - } - } - } - waitNextMinute() + slog.Info("Received events", slog.Int("count", len(events.Items)), slog.Any("func", configs.CallerInfo())) + case <-loadTicker.C: + go Load(c) + case <-checkTicker.C: + Check(events, cfg) // Check in this goroutine to prevent unsynchronized access to events } } } -// Load loads events from the local calendar and sends them to the provided channel. -func Load(ch chan<- *configs.Events, errch chan<- error) { - d, err := platform.ConfigDir() +// Load loads local calendar events from the configuration file. +func Load(c chan<- *configs.Events) { + f, err := configs.OpenConfig(os.O_RDONLY) if err != nil { - errch <- err + slog.Error(err.Error(), slog.Any("func", configs.CallerInfo())) return } + defer f.Close() - file, err := os.OpenFile(filepath.Join(d, configs.ConfigFile), os.O_CREATE|os.O_RDONLY, 0640) - if err != nil { - errch <- err + events := new(configs.Events) + if err = json.NewDecoder(f).Decode(events); err != nil { + slog.Error(err.Error(), slog.Any("func", configs.CallerInfo())) return } - defer file.Close() - for { - var events configs.Events - _, err := file.Seek(0, 0) - if err != nil { - errch <- err - return - } + c <- events +} - err = json.NewDecoder(file).Decode(&events) - if err != nil { - errch <- err - return +// Check checks all events for a match with the current day and time and executes actions accordingly. +func Check(events *configs.Events, cfg *configs.Config) { + for _, e := range events.Items { + if Match(e) { + if err := Execute(e, cfg.AutoStart); err != nil { + slog.Error(err.Error(), slog.Any("func", configs.CallerInfo())) + } } - - ch <- &events - waitNextMinute() } } -// Match checks if the given event matches the current time(hh:mm) and day. -func Match(event *configs.Event) bool { +// Match checks if the calendar event matches the current day and time. +func Match(e *configs.Event) (ok bool) { now := time.Now() - for _, d := range event.Start.Days { + + for _, d := range e.Start.Days { if d == now.Weekday().String() { - slog.Info("Match", - slog.String("event.time", event.Start.Time), - slog.String("now.time", now.Format("15:04")), - CalAttr, + slog.Debug("Matching event", + slog.String("now", now.Format("15:04")), + slog.String("then", e.Start.Time), + slog.Any("func", configs.CallerInfo()), ) - return event.Start.Time == now.Format("15:04") + return e.Start.Time == now.Format("15:04") } } return false } -// Execute executes actions associated with the given event, such as notifying and potentially starting a meeting. -func Execute(event *configs.Event, autoStart bool) error { - if err := platform.Notify(event.Summary, event.Url); err != nil { - slog.Error(err.Error()) +// Execute executes actions based on the given calendar event. +func Execute(e *configs.Event, autoStart bool) error { + if err := platform.Notify(e.Summary, e.Url); err != nil { + slog.Error(err.Error(), slog.Any("func", configs.CallerInfo())) } if autoStart { - return platform.OpenURL(event.Url) + return platform.OpenURL(e.Url) } + return nil } diff --git a/internal/platform/darwin.go b/internal/platform/darwin.go index d9de585..ce78ab0 100644 --- a/internal/platform/darwin.go +++ b/internal/platform/darwin.go @@ -10,33 +10,29 @@ import ( "path/filepath" ) -const ( - ToolName = "gomeet" -) +const ToolName = "gomeet" -var ( - ErrNotImpl = errors.New("not implemented on Linux platform") -) +var ErrNotImplemented = errors.New("feature: not implemented on Linux platform") -// Notify sends a meeting notification with the specified summary and URL. +// Notify displays a notification with the given summary and URL. func Notify(summary string, url string) error { - return ErrNotImpl + return ErrNotImplemented } -// OpenURL opens the specified URL in the default web browser. +// OpenURL opens the provided URL in the default web browser. func OpenURL(url string) error { cmd := exec.Command("open", url) return cmd.Run() } -// LogDir returns the directory path for storing logs related to the tool. -func LogDir() (string, error) { - h, err := os.UserHomeDir() +// LogDir returns the path to the log directory. +func LogDir() (path string, err error) { + d, err := os.UserHomeDir() if err != nil { return "", err } - d := filepath.Join(h, "Library", "Logs", ToolName) + d = filepath.Join(d, "Library", "Logs", ToolName) if err := os.MkdirAll(d, 0750); err != nil { return "", err } @@ -44,14 +40,14 @@ func LogDir() (string, error) { return d, nil } -// ConfigDir returns the directory path for storing configuration files related to the tool. -func ConfigDir() (string, error) { - c, err := os.UserConfigDir() +// ConfigDir returns the path to the configuration directory. +func ConfigDir() (path string, err error) { + d, err := os.UserConfigDir() if err != nil { return "", err } - d := filepath.Join(c, ToolName) + d = filepath.Join(d, ToolName) if err := os.MkdirAll(d, 0750); err != nil { return "", err } diff --git a/internal/platform/linux.go b/internal/platform/linux.go index 9517a09..7580e49 100644 --- a/internal/platform/linux.go +++ b/internal/platform/linux.go @@ -10,27 +10,23 @@ import ( "path/filepath" ) -const ( - ToolName = "gomeet" -) +const ToolName = "gomeet" -var ( - ErrNotImpl = errors.New("not implemented on Linux platform") -) +var ErrNotImplemented = errors.New("feature: not implemented on Linux platform") -// Notify sends a meeting notification with the specified summary and URL. +// Notify displays a notification with the given summary and URL. func Notify(summary string, url string) error { - return ErrNotImpl + return ErrNotImplemented } -// OpenURL opens the specified URL in the default web browser. +// OpenURL opens the provided URL in the default web browser. func OpenURL(url string) error { cmd := exec.Command("xdg-open", url) return cmd.Run() } -// LogDir returns the directory path for storing logs related to the tool. -func LogDir() (string, error) { +// LogDir returns the path to the log directory. +func LogDir() (path string, err error) { d := filepath.Join("/", "var", "log", ToolName) if err := os.MkdirAll(d, 0750); err != nil { return "", err @@ -39,14 +35,14 @@ func LogDir() (string, error) { return d, nil } -// ConfigDir returns the directory path for storing configuration files related to the tool. -func ConfigDir() (string, error) { - c, err := os.UserConfigDir() +// ConfigDir returns the path to the configuration directory. +func ConfigDir() (path string, err error) { + d, err := os.UserConfigDir() if err != nil { return "", err } - d := filepath.Join(c, ToolName) + d = filepath.Join(d, ToolName) if err := os.MkdirAll(d, 0750); err != nil { return "", err } diff --git a/internal/platform/windows.go b/internal/platform/windows.go index f9eb039..fba2d59 100644 --- a/internal/platform/windows.go +++ b/internal/platform/windows.go @@ -12,37 +12,36 @@ import ( "github.com/go-toast/toast" ) -const ( - ToolName = "gomeet" -) +const ToolName = "gomeet" +const LocalDirKey = "LOCALAPPDATA" -// Notify sends a meeting notification with the specified summary and URL. +// Notify displays a notification with the given summary and URL. func Notify(summary string, url string) error { - notification := toast.Notification{ - AppID: "gomeet", + n := toast.Notification{ + AppID: ToolName, Title: "Join Meeting: " + summary, Message: "Click to join the meeting now.", Actions: []toast.Action{{Type: "protocol", Label: "Join", Arguments: url}}, Duration: toast.Long, } - return notification.Push() + return n.Push() } -// OpenURL opens the specified URL in the default web browser. +// OpenURL opens the provided URL in the default web browser. func OpenURL(url string) error { cmd := exec.Command("cmd", "/c", "start", url) return cmd.Run() } -// LogDir returns the directory path for storing logs related to the tool. -func LogDir() (string, error) { - l := os.Getenv("LOCALAPPDATA") - if l == "" { - return "", fmt.Errorf("'LOCALAPPDATA' is not defined in the environment variables") +// LogDir returns the path to the log directory. +func LogDir() (path string, err error) { + d := os.Getenv(LocalDirKey) + if d == "" { + return "", fmt.Errorf("'%s' environment variable is not set", LocalDirKey) } - d := filepath.Join(l, ToolName, "logs") + d = filepath.Join(d, ToolName, "logs") if err := os.MkdirAll(d, 0750); err != nil { return "", err } @@ -50,14 +49,14 @@ func LogDir() (string, error) { return d, nil } -// ConfigDir returns the directory path for storing configuration files related to the tool. -func ConfigDir() (string, error) { - c, err := os.UserConfigDir() +// ConfigDir returns the path to the configuration directory. +func ConfigDir() (path string, err error) { + d, err := os.UserConfigDir() if err != nil { return "", err } - d := filepath.Join(c, ToolName) + d = filepath.Join(d, ToolName) if err := os.MkdirAll(d, 0750); err != nil { return "", err }