diff --git a/cmd/enbas/main.go b/cmd/enbas/main.go index c288602..d74d545 100644 --- a/cmd/enbas/main.go +++ b/cmd/enbas/main.go @@ -30,12 +30,14 @@ func main() { func run() error { var ( configDir string + cacheDir string pager string maxTerminalWidth int noColor *bool ) flag.StringVar(&configDir, "config-dir", "", "Specify your config directory") + flag.StringVar(&cacheDir, "cache-dir", "", "Specify your cache directory") flag.StringVar(&pager, "pager", "", "Specify your preferred pager to page through long outputs. This is disabled by default.") flag.IntVar(&maxTerminalWidth, "max-terminal-width", 80, "Specify the maximum terminal width when displaying resources on screen.") @@ -170,6 +172,7 @@ func run() error { executor.CommandShow: executor.NewShowExecutor( printer, configDir, + cacheDir, executor.CommandShow, executor.CommandSummaryLookup(executor.CommandShow), ), diff --git a/internal/client/client.go b/internal/client/client.go index ea006e7..71c376d 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -60,6 +60,10 @@ func (g *Client) AuthCodeURL() string { ) } +func (g *Client) DownloadFile(url, path string) error { + return nil +} + func (g *Client) sendRequest(method string, url string, requestBody io.Reader, object any) error { ctx, cancel := context.WithTimeout(context.Background(), g.Timeout) defer cancel() diff --git a/internal/config/credentials.go b/internal/config/credentials.go index 56bb077..47f6bef 100644 --- a/internal/config/credentials.go +++ b/internal/config/credentials.go @@ -11,6 +11,8 @@ import ( "os" "path/filepath" "strings" + + "codeflow.dananglin.me.uk/apollo/enbas/internal/utilities" ) const ( @@ -41,7 +43,7 @@ func (e CredentialsNotFoundError) Error() string { // directory. If the directory is not specified then the default directory is used. If the directory // is not present, it will be created. func SaveCredentials(configDir, username string, credentials Credentials) (string, error) { - if err := ensureConfigDir(calculateConfigDir(configDir)); err != nil { + if err := utilities.EnsureDirectory(utilities.CalculateConfigDir(configDir)); err != nil { return "", fmt.Errorf("unable to ensure the configuration directory: %w", err) } @@ -141,5 +143,5 @@ func saveCredentialsConfigFile(authConfig CredentialsConfig, configDir string) e } func credentialsConfigFile(configDir string) string { - return filepath.Join(calculateConfigDir(configDir), credentialsFileName) + return filepath.Join(utilities.CalculateConfigDir(configDir), credentialsFileName) } diff --git a/internal/config/directory.go b/internal/config/directory.go deleted file mode 100644 index 4cad91b..0000000 --- a/internal/config/directory.go +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Dan Anglin -// -// SPDX-License-Identifier: GPL-3.0-or-later - -package config - -import ( - "errors" - "fmt" - "os" - "path/filepath" - - "codeflow.dananglin.me.uk/apollo/enbas/internal" -) - -func calculateConfigDir(configDir string) string { - if configDir != "" { - return configDir - } - - rootDir, err := os.UserConfigDir() - if err != nil { - rootDir = "." - } - - return filepath.Join(rootDir, internal.ApplicationName) -} - -func ensureConfigDir(configDir string) error { - if _, err := os.Stat(configDir); err != nil { - if errors.Is(err, os.ErrNotExist) { - if err := os.MkdirAll(configDir, 0o750); err != nil { - return fmt.Errorf("unable to create %s: %w", configDir, err) - } - } else { - return fmt.Errorf("unknown error received after getting the config directory information: %w", err) - } - } - - return nil -} diff --git a/internal/executor/show.go b/internal/executor/show.go index 1c01162..1ee9a17 100644 --- a/internal/executor/show.go +++ b/internal/executor/show.go @@ -5,8 +5,11 @@ package executor import ( + "errors" "flag" "fmt" + "path/filepath" + "strings" "codeflow.dananglin.me.uk/apollo/enbas/internal/client" "codeflow.dananglin.me.uk/apollo/enbas/internal/model" @@ -23,6 +26,7 @@ type ShowExecutor struct { showUserPreferences bool showInBrowser bool configDir string + cacheRoot string resourceType string accountName string statusID string @@ -31,15 +35,17 @@ type ShowExecutor struct { tag string pollID string attachmentID string + fromResourceType string limit int } -func NewShowExecutor(printer *printer.Printer, configDir, name, summary string) *ShowExecutor { +func NewShowExecutor(printer *printer.Printer, configDir, cacheRoot, name, summary string) *ShowExecutor { showExe := ShowExecutor{ FlagSet: flag.NewFlagSet(name, flag.ExitOnError), printer: printer, configDir: configDir, + cacheRoot: cacheRoot, } showExe.BoolVar(&showExe.myAccount, flagMyAccount, false, "Set to true to lookup your account") @@ -54,6 +60,7 @@ func NewShowExecutor(printer *printer.Printer, configDir, name, summary string) showExe.StringVar(&showExe.tag, flagTag, "", "Specify the name of the tag to use") showExe.StringVar(&showExe.pollID, flagPollID, "", "Specify the ID of the poll to display") showExe.StringVar(&showExe.attachmentID, flagAttachmentID, "", "Specify the ID of the media attachment to display") + showExe.StringVar(&showExe.fromResourceType, flagFrom, "", "Specify the resource type to view the target resource from (e.g. status for viewing media from, etc)") showExe.IntVar(&showExe.limit, flagLimit, 20, "Specify the limit of items to display") showExe.Usage = commandUsageFunc(name, summary, showExe.FlagSet) @@ -81,7 +88,7 @@ func (s *ShowExecutor) Execute() error { resourceFollowRequest: s.showFollowRequests, resourcePoll: s.showPoll, resourceMutedAccounts: s.showMutedAccounts, - resourceMedia: s.showMediaAttachment, + resourceMedia: s.showMedia, resourceMediaAttachment: s.showMediaAttachment, } @@ -421,3 +428,81 @@ func (s *ShowExecutor) showMediaAttachment(gtsClient *client.Client) error { return nil } + +func (s *ShowExecutor) showMedia(gtsClient *client.Client) error { + if s.fromResourceType == "" { + return FlagNotSetError{flagText: flagFrom} + } + + funcMap := map[string]func(*client.Client) error{ + resourceStatus: s.showMediaFromStatus, + } + + doFunc, ok := funcMap[s.fromResourceType] + if !ok { + return fmt.Errorf("do not support viewing media from %s", s.fromResourceType) + } + + return doFunc(gtsClient) +} + +func (s *ShowExecutor) showMediaFromStatus(gtsClient *client.Client) error { + if s.attachmentID == "" { + return FlagNotSetError{flagText: flagAttachmentID} + } + + if s.statusID == "" { + return FlagNotSetError{flagText: flagStatusID} + } + + status, err := gtsClient.GetStatus(s.statusID) + if err != nil { + return fmt.Errorf("unable to retrieve the status: %w", err) + } + + attachmentExists := false + mediaURL := "" + mediaFilename := "" + + for _, attachment := range status.MediaAttachments { + if attachment.ID == s.attachmentID { + mediaURL = attachment.URL + split := strings.Split(attachment.URL, "/") + mediaFilename = split[len(split)-1] + attachmentExists = true + + break + } + } + + if !attachmentExists { + return errors.New("this media is not attached to this status") + } + + cacheDir := filepath.Join( + utilities.CalculateCacheDir(s.cacheRoot, utilities.GetFQDN(gtsClient.Authentication.Instance)), + "media", + ) + + if err := utilities.EnsureDirectory(cacheDir); err != nil { + return fmt.Errorf("unable to ensure the existence of the directory %q: %w", cacheDir, err) + } + + mediaFilePath := filepath.Join(cacheDir, mediaFilename) + + fileExists, err := utilities.FileExists(mediaFilePath) + if err != nil { + return fmt.Errorf("unable to check if the media file is already downloaded: %w", err) + } + + if !fileExists { + if err := utilities.DownloadFile(mediaURL, mediaFilePath); err != nil { + return fmt.Errorf("unable to download the media attachment: %w", err) + } + } + + // TODO: display the file in the image viewer. + fmt.Println("checkpoint: the media file is now on disk; just need to open it in an image viewer...") + + return nil +} diff --git a/internal/utilities/directories.go b/internal/utilities/directories.go new file mode 100644 index 0000000..0307efe --- /dev/null +++ b/internal/utilities/directories.go @@ -0,0 +1,50 @@ +package utilities + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "codeflow.dananglin.me.uk/apollo/enbas/internal" +) + +func CalculateConfigDir(configDir string) string { + if configDir != "" { + return configDir + } + + configRoot, err := os.UserConfigDir() + if err != nil { + configRoot = "." + } + + return filepath.Join(configRoot, internal.ApplicationName) +} + +func CalculateCacheDir(cacheDir, instanceFQDN string) string { + if cacheDir != "" { + return cacheDir + } + + cacheRoot, err := os.UserCacheDir() + if err != nil { + cacheRoot = "." + } + + return filepath.Join(cacheRoot, internal.ApplicationName, instanceFQDN) +} + +func EnsureDirectory(dir string) error { + if _, err := os.Stat(dir); err != nil { + if errors.Is(err, os.ErrNotExist) { + if err := os.MkdirAll(dir, 0o750); err != nil { + return fmt.Errorf("unable to create %s: %w", dir, err) + } + } else { + return fmt.Errorf("received an unknown error after getting the directory information: %w", err) + } + } + + return nil +} diff --git a/internal/utilities/file.go b/internal/utilities/file.go index 99fc67b..cdc7aef 100644 --- a/internal/utilities/file.go +++ b/internal/utilities/file.go @@ -5,8 +5,12 @@ package utilities import ( + "errors" "fmt" + "io" + "net/http" "os" + "time" ) func ReadFile(path string) (string, error) { @@ -17,3 +21,45 @@ func ReadFile(path string) (string, error) { return string(data), nil } + +func FileExists(path string) (bool, error) { + if _, err := os.Stat(path); err != nil { + if errors.Is(err, os.ErrNotExist) { + return false, nil + } + + return false, fmt.Errorf("unable to check if the file exists: %w", err) + } + + return true, nil +} + +// TODO: move to client package +func DownloadFile(url, path string) error { + client := http.Client{ + CheckRedirect: func(r *http.Request, _ []*http.Request) error { + r.URL.Opaque = r.URL.Path + + return nil + }, + Timeout: time.Second * 30, + } + + resp, err := client.Get(url) + if err != nil { + return fmt.Errorf("unable to download the file: %w", err) + } + defer resp.Body.Close() + + file, err := os.Create(path) + if err != nil { + return fmt.Errorf("unable to create %s: %w", path, err) + } + defer file.Close() + + if _, err = io.Copy(file, resp.Body); err != nil { + return fmt.Errorf("unable to save the download to %s: %w", path, err) + } + + return nil +} diff --git a/internal/utilities/utilities.go b/internal/utilities/utilities.go new file mode 100644 index 0000000..ab535fc --- /dev/null +++ b/internal/utilities/utilities.go @@ -0,0 +1,11 @@ +package utilities + +import ( + "regexp" +) + +func GetFQDN(url string) string { + r := regexp.MustCompile(`http(s)?:\/\/`) + + return r.ReplaceAllString(url, "") +}