From 248e77cba1203a48dd4033c1ca46334721e2ec34 Mon Sep 17 00:00:00 2001 From: mr-pmillz <> Date: Mon, 9 Sep 2024 21:02:12 -0400 Subject: [PATCH] added local file caching --- README.md | 27 ++++------- cache.go | 81 +++++++++++++++++++++++++++++++++ cmd/eoldate/main.go | 12 ++--- eoldate.go | 84 ++++++++++++++++++++--------------- examples/is-supported/main.go | 25 ++++------- utils.go | 74 ++++++++++++++++++++++++++++++ 6 files changed, 222 insertions(+), 81 deletions(-) create mode 100644 cache.go diff --git a/README.md b/README.md index 1b180a7..05c092d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ This is a wrapper around the endoflife.date API ## Installation -To install, just run the below command or download pre-compiled binary from the [releases page](https://github.com/mr-pmillz/eoldate/releases) +To install, run the following command or download a pre-compiled binary from the [releases page](https://github.com/mr-pmillz/eoldate/releases) ```shell go install -v github.com/mr-pmillz/eoldate/cmd/eoldate@latest @@ -58,25 +58,16 @@ import ( func main() { client := eoldate.NewClient() - products, err := client.GetProduct("php") + softwareName := "php" + phpVersion := 8.2 + isPHPEightPointTwoSupported, err := client.IsSupportedSoftwareVersion(softwareName, phpVersion) if err != nil { - log.Fatalf("Error fetching product data: %v", err) + log.Fatal(err) } - - versionsToCheck := []float64{5.6, 7.4, 8.0, 8.1, 8.2} - - for _, version := range versionsToCheck { - supported, err := products.IsVersionSupported(version) - if err != nil { - fmt.Printf("PHP %.1f: %v\n", version, err) - continue - } - - if supported { - fmt.Printf("PHP %.1f is still supported\n", version) - } else { - fmt.Printf("PHP %.1f is no longer supported\n", version) - } + if isPHPEightPointTwoSupported { + fmt.Printf("%s %.1f is Supported", softwareName, phpVersion) + } else { + fmt.Printf("%s %.1f is not Supported", softwareName, phpVersion) } } ``` \ No newline at end of file diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..79ad86a --- /dev/null +++ b/cache.go @@ -0,0 +1,81 @@ +package eoldate + +import ( + "fmt" + "os" + "path/filepath" + "time" +) + +// readCache reads the cached data for a product +func readCache(product string) ([]byte, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, LogError(err) + } + timestamp := time.Now().Format("01-02-2006") + cacheDir := filepath.Join(homeDir, ".config", "eoldate", "cache") + cacheFile := fmt.Sprintf("%s/%s-%s.json", cacheDir, product, timestamp) + if exists, err := Exists(cacheFile); err == nil && exists { + return os.ReadFile(cacheFile) + } else { + return nil, nil + } +} + +// readAllTechnologiesCache ... +func readAllTechnologiesCache() ([]string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, LogError(err) + } + timestamp := time.Now().Format("01-02-2006") + cacheDir := filepath.Join(homeDir, ".config", "eoldate", "cache") + cacheAllTechFile := fmt.Sprintf("%s/all-technologies-%s.json", cacheDir, timestamp) + if exists, err := Exists(cacheAllTechFile); err == nil && exists { + return ReadLines(cacheAllTechFile) + } else { + return nil, nil + } +} + +// writeCache writes data to the cache for a product +func writeCache(product string, data []byte) error { + homeDir, err := os.UserHomeDir() + if err != nil { + return LogError(err) + } + timestamp := time.Now().Format("01-02-2006") + cacheDir := filepath.Join(homeDir, ".config", "eoldate", "cache") + cacheFile := fmt.Sprintf("%s/%s-%s.json", cacheDir, product, timestamp) + return os.WriteFile(cacheFile, data, 0600) +} + +// CacheTechnologies caches all available technologies to choose from to a local file cache +func (c *Client) CacheTechnologies() ([]string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, LogError(err) + } + timestamp := time.Now().Format("01-02-2006") + cacheDir := filepath.Join(homeDir, ".config", "eoldate", "cache") + allTechnologiesFileCache := fmt.Sprintf("%s/all-technologies-%s.json", cacheDir, timestamp) + if exists, err := Exists(cacheDir); err == nil && !exists { + if err = os.MkdirAll(cacheDir, 0755); err != nil { + return nil, LogError(err) + } + } + + if cacheExists, err := Exists(allTechnologiesFileCache); err == nil && cacheExists { + return ReadLines(allTechnologiesFileCache) + } + allProducts, err := c.GetAllProducts() + if err != nil { + return nil, LogError(err) + } + if err = WriteLines(allProducts, allTechnologiesFileCache); err != nil { + return nil, LogError(err) + } + + return allProducts, nil +} diff --git a/cmd/eoldate/main.go b/cmd/eoldate/main.go index a16d290..271cc1a 100644 --- a/cmd/eoldate/main.go +++ b/cmd/eoldate/main.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "flag" "fmt" "github.com/olekukonko/tablewriter" @@ -63,22 +62,17 @@ func main() { os.Exit(1) } - data, err := client.Get(fmt.Sprintf("%s.json", eolOptions.Tech)) + data, err := client.GetProduct(eolOptions.Tech) if err != nil { gologger.Fatal().Msgf("Error fetching product data: %v", err) } - var products []eoldate.Product - if err = json.Unmarshal(data, &products); err != nil { - gologger.Fatal().Msgf("Error parsing JSON: %v", err) - } - - tableBuilder := NewTableBuilder(products) + tableBuilder := NewTableBuilder(data) tableString := tableBuilder.Render() fmt.Println(tableString) if eolOptions.Output != "" { - writeOutputFiles(eolOptions, tableString, products) + writeOutputFiles(eolOptions, tableString, data) } } diff --git a/eoldate.go b/eoldate.go index 0119b19..54df2bf 100644 --- a/eoldate.go +++ b/eoldate.go @@ -5,12 +5,13 @@ import ( "fmt" "io" "net/http" + "slices" "strconv" "time" ) const ( - CurrentVersion = `v0.0.9` + CurrentVersion = `v1.0.0` EOLBaseURL = "https://endoflife.date/api" NotAvailable = "N/A" ) @@ -39,38 +40,6 @@ type Product struct { AdditionalFields map[string]interface{} `json:"-"` } -// UnmarshalJSON implements the json.Unmarshaler interface -func (p *Product) UnmarshalJSON(data []byte) error { - type ProductAlias Product - alias := &struct { - *ProductAlias - AdditionalFields map[string]interface{} `json:"-"` - }{ - ProductAlias: (*ProductAlias)(p), - } - - if err := json.Unmarshal(data, &alias); err != nil { - return err - } - - var raw map[string]interface{} - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - - p.AdditionalFields = make(map[string]interface{}) - for k, v := range raw { - switch k { - case "cycle", "releaseDate", "eol", "latest", "link", "latestReleaseDate", "lts", "support", "extendedSupport", "minJavaVersion", "supportedPHPVersions": - // These fields are already handled by the struct - default: - p.AdditionalFields[k] = v - } - } - - return nil -} - // GetSupportedPHPVersions returns the supported PHP versions as a string func (p *Product) GetSupportedPHPVersions() string { switch v := p.SupportedPHPVersions.(type) { @@ -83,6 +52,19 @@ func (p *Product) GetSupportedPHPVersions() string { } } +// IsSupportedSoftwareVersion ... +func (c *Client) IsSupportedSoftwareVersion(softwareName string, version float64) (bool, error) { + softwareReleaseData, err := c.GetProduct(softwareName) + if err != nil { + return false, LogError(err) + } + isSupported, err := softwareReleaseData.IsVersionSupported(version) + if err != nil { + return false, LogError(err) + } + return isSupported, nil +} + // IsVersionSupported checks if the given version is supported in any of the product cycles func (p Products) IsVersionSupported(version float64) (bool, error) { for _, product := range p { @@ -165,18 +147,46 @@ type Products []Product // GetProduct fetches the end-of-life information for a specific product. func (c *Client) GetProduct(product string) (Products, error) { - data, err := c.Get(fmt.Sprintf("%s.json", product)) + allProducts, err := c.CacheTechnologies() if err != nil { return nil, err } + if slices.Contains(allProducts, product) { + var products Products + productCache, err := readCache(product) + if err != nil { + return nil, err + } + if productCache != nil { + err = json.Unmarshal(productCache, &products) + return products, err + } + data, err := c.Get(fmt.Sprintf("%s.json", product)) + if err != nil { + return nil, err + } - var products Products - err = json.Unmarshal(data, &products) - return products, err + if err = json.Unmarshal(data, &products); err != nil { + return nil, err + } + if err = writeCache(product, data); err != nil { + return nil, err + } + return products, err + } else { + return nil, fmt.Errorf("product %s not found", product) + } } // GetAllProducts fetches the end-of-life information for all products. func (c *Client) GetAllProducts() (AllProducts, error) { + allProductsCache, err := readAllTechnologiesCache() + if err != nil { + return nil, LogError(err) + } + if allProductsCache != nil { + return allProductsCache, nil + } data, err := c.Get("all.json") if err != nil { return nil, err diff --git a/examples/is-supported/main.go b/examples/is-supported/main.go index 7a4bf3c..e86439b 100644 --- a/examples/is-supported/main.go +++ b/examples/is-supported/main.go @@ -9,24 +9,15 @@ import ( func main() { client := eoldate.NewClient() - products, err := client.GetProduct("php") + softwareName := "php" + phpVersion := 8.2 + isPHPEightPointTwoSupported, err := client.IsSupportedSoftwareVersion(softwareName, phpVersion) if err != nil { - log.Fatalf("Error fetching product data: %v", err) + log.Fatal(err) } - - versionsToCheck := []float64{5.6, 7.4, 8.0, 8.1, 8.2} - - for _, version := range versionsToCheck { - supported, err := products.IsVersionSupported(version) - if err != nil { - fmt.Printf("PHP %.1f: %v\n", version, err) - continue - } - - if supported { - fmt.Printf("PHP %.1f is still supported\n", version) - } else { - fmt.Printf("PHP %.1f is no longer supported\n", version) - } + if isPHPEightPointTwoSupported { + fmt.Printf("%s %.1f is Supported", softwareName, phpVersion) + } else { + fmt.Printf("%s %.1f is not Supported", softwareName, phpVersion) } } diff --git a/utils.go b/utils.go index a5b9d53..6a083a4 100644 --- a/utils.go +++ b/utils.go @@ -1,7 +1,9 @@ package eoldate import ( + "bufio" "encoding/json" + "fmt" "github.com/gocarina/gocsv" "os" "os/user" @@ -9,6 +11,78 @@ import ( "strings" ) +// Exists returns whether the given file or directory exists +func Exists(path string) (bool, error) { + if path == "" { + return false, nil + } + absPath, err := ResolveAbsPath(path) + if err != nil { + return false, err + } + info, err := os.Stat(absPath) + if err == nil { + switch { + case info.IsDir(): + return true, nil + case info.Size() >= 0: + // file exists but it's empty + return true, nil + } + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +// WriteLines writes the lines to the given file. +func WriteLines(lines []string, path string) error { + file, err := os.Create(path) + if err != nil { + return LogError(err) + } + defer file.Close() + + w := bufio.NewWriter(file) + for _, line := range lines { + if len(line) > 0 { + _, _ = fmt.Fprintln(w, line) + } + } + return w.Flush() +} + +// ReadLines reads a whole file into memory +// and returns a slice of its lines. +func ReadLines(path string) ([]string, error) { + var lines []string + absPath, err := ResolveAbsPath(path) + if err != nil { + return nil, LogError(err) + } + exists, err := Exists(absPath) + if err != nil { + return nil, LogError(err) + } + if !exists { + fmt.Printf("File does not exist, cannot read lines for non-existent file: %s", absPath) + return lines, nil + } + + file, err := os.Open(absPath) + if err != nil { + return nil, LogError(err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + return lines, scanner.Err() +} + // WriteStructToJSONFile ... func WriteStructToJSONFile(data interface{}, outputFile string) error { outputFileDir := filepath.Dir(outputFile)