diff --git a/.idea/runConfigurations/Clean.xml b/.idea/runConfigurations/Clean.xml
new file mode 100644
index 0000000..5ba1469
--- /dev/null
+++ b/.idea/runConfigurations/Clean.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Run.xml b/.idea/runConfigurations/Run.xml
index 8bbc776..9fd20fc 100644
--- a/.idea/runConfigurations/Run.xml
+++ b/.idea/runConfigurations/Run.xml
@@ -2,8 +2,8 @@
-
-
+
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/Makefile b/Makefile
index 0f35e72..d257d44 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,33 @@
+all: clean lint test build
+
+.PHONY: build
+build: main.go cmd output redis stats
+ @echo Building
+ @go build -o drupal_redis_stats
+
+.PHONY: clean
+clean:
+ @echo Cleaning
+ @rm -f coverage* drupal_redis_stats
+
+.PHONY: install
+install: main.go cmd/credentials.go cmd/wire.go cmd/wire_gen.go output redis stats
+ @echo Installing
+ @go install
+
.PHONY: lint
-lint:
- find . -type f -name "*.go" | xargs gofmt -l
- find . -type f -name "*.go" | xargs goimports -l
- staticcheck ./...
+lint: cmd/wire_gen.go
+ @echo Linting
+ @find . -type f -name "*.go" | xargs gofmt -l
+ @find . -type f -name "*.go" | xargs goimports -l
+ @golint ./...
+ @staticcheck ./...
+
+.PHONY: test
+test: cmd output redis stats cmd/wire_gen.go
+ @echo Testing
+ @go test -race ./...
+
+cmd/wire_gen.go: cmd/wire.go redis stats
+ @echo Wiring
+ @go generate ./...
diff --git a/auth.go b/cmd/credentials.go
similarity index 54%
rename from auth.go
rename to cmd/credentials.go
index 37af1f6..4c1f18c 100644
--- a/auth.go
+++ b/cmd/credentials.go
@@ -1,18 +1,21 @@
-package main
+package cmd
import (
"errors"
- "flag"
"fmt"
"io"
"net/url"
"os"
"unicode/utf8"
- "github.com/gomodule/redigo/redis"
+ "github.com/spf13/pflag"
"golang.org/x/term"
+
+ "github.com/fgm/drupal_redis_stats/redis"
)
+// PasswordReader abstracts the term.ReadPassword function, to allow switching
+// it for a version not depending on termios during tests.
type PasswordReader interface {
ReadPassword(int) ([]byte, error)
}
@@ -23,10 +26,11 @@ func (prf passwordReaderFunc) ReadPassword(fd int) ([]byte, error) {
return prf(fd)
}
+// ErrInvalidUTF8 is raised when a string is not valid.
var ErrInvalidUTF8 = errors.New("input was not a valid UTF-8 string")
-func getPasswordFromCLI(w io.Writer, pr PasswordReader, fd int) (string, error) {
- fmt.Fprint(w, "Password: ")
+func getPasswordFromCLI(w io.Writer, pr PasswordReader, fd int) (redis.PassValue, error) {
+ _, _ = fmt.Fprint(w, "Password: ")
pBytes, err := pr.ReadPassword(fd)
if err != nil {
return "", fmt.Errorf("failed acquiring password from terminal without echo: %w", err)
@@ -34,7 +38,7 @@ func getPasswordFromCLI(w io.Writer, pr PasswordReader, fd int) (string, error)
if !utf8.Valid(pBytes) {
return "", fmt.Errorf("invalid password: %w", ErrInvalidUTF8)
}
- return string(pBytes), nil
+ return redis.PassValue(pBytes), nil
}
// getCredentials combines the user and pass values with those possibly
@@ -42,54 +46,44 @@ func getPasswordFromCLI(w io.Writer, pr PasswordReader, fd int) (string, error)
// terminal in echo-less mode.
//
// Explicit user/pass flags override those found in the DSN.
-func getCredentials(fs *flag.FlagSet, w io.Writer, flagDSN, flagUser, flagPass string) (user, pass string, err error) {
- user, pass = flagUser, flagPass
-
- hasDSNFlag := isFlagPassed(fs, "dsn")
- hasUserFlag := isFlagPassed(fs, "user")
- hasPassFlag := isFlagPassed(fs, "pass")
+func getCredentials(fs *pflag.FlagSet, w io.Writer, dsn redis.DSNValue, flagUser redis.UserValue, flagPass redis.PassValue) (user redis.UserValue, pass redis.PassValue, err error) {
+ hasDSNFlag := redis.IsFlagPassed(fs, "dsn")
+ hasUserFlag := redis.IsFlagPassed(fs, "user")
+ hasPassFlag := redis.IsFlagPassed(fs, "pass")
if hasDSNFlag && (!hasUserFlag || !hasPassFlag) {
var u *url.URL
- u, err = url.Parse(flagDSN)
+ u, err = url.Parse(string(dsn))
if err != nil {
return "", "", fmt.Errorf("failed parsing Redis DSN: %v", err)
}
if !hasUserFlag {
- user = u.User.Username()
+ user = redis.UserValue(u.User.Username())
}
if !hasPassFlag {
dsnPass, DSNContainsPass := u.User.Password()
if DSNContainsPass {
- pass = dsnPass
+ pass = redis.PassValue(dsnPass)
} else {
// Redis URL parsing accepts single auth element as password, not user
- pass = user
+ pass = redis.PassValue(user)
user = ""
}
}
}
-
- if hasPassFlag && flagPass == "" {
- pass, err = getPasswordFromCLI(w, passwordReaderFunc(term.ReadPassword), int(os.Stdin.Fd()))
- if err != nil {
- return "", "", err
- }
- }
- return
-}
-
-func authenticate(c redis.Conn, includeUser bool, user, pass string) error {
- var err error
- if includeUser {
- _, err = c.Do("AUTH", user, pass)
- } else {
- _, err = c.Do("AUTH", pass)
+ if hasUserFlag {
+ user = flagUser
}
- if err != nil {
- return fmt.Errorf("failed AUTH: %w", err)
+ if hasPassFlag {
+ if flagPass == "" {
+ pass, err = getPasswordFromCLI(w, passwordReaderFunc(term.ReadPassword), int(os.Stdin.Fd()))
+ if err != nil {
+ return "", "", err
+ }
+ } else {
+ pass = flagPass
+ }
}
-
- return nil
+ return user, pass, nil
}
diff --git a/auth_test.go b/cmd/credentials_test.go
similarity index 75%
rename from auth_test.go
rename to cmd/credentials_test.go
index ea98873..b4b30cf 100644
--- a/auth_test.go
+++ b/cmd/credentials_test.go
@@ -1,15 +1,17 @@
-package main
+package cmd
import (
"errors"
- "flag"
"fmt"
"io"
"os"
"syscall"
"testing"
+ "github.com/spf13/pflag"
"golang.org/x/term"
+
+ "github.com/fgm/drupal_redis_stats/redis"
)
const testPassword = "test-password"
@@ -27,7 +29,7 @@ func TestGetPasswordFromCLI(t *testing.T) {
name string
reader PasswordReader
input io.Reader
- expValue string
+ expValue redis.PassValue
expError error
}{
{"test happy", readTestPassword(testPassword), f, testPassword, nil},
@@ -60,25 +62,26 @@ func TestGetPasswordFromCLI(t *testing.T) {
}
func TestGetCredentials(t *testing.T) {
checks := [...]struct {
- name string
- args []string
- expUser, expPass string
- expError bool
+ name string
+ args []string
+ expUser redis.UserValue
+ expPass redis.PassValue
+ expError bool
}{
{"nothing, nil", nil, "", "", false},
- {"bad dsn", []string{"-dsn", ":localhost/0"}, "", "", true},
- {"dsn, no info", []string{"-dsn", "redis://localhost/0"}, "", "", false},
- {"dsn, only pass", []string{"-dsn", "redis://pass@localhost/0"}, "", "pass", false},
- {"dsn, only empty pass", []string{"-dsn", "redis://@localhost/0"}, "", "", false},
- {"dsn, user+pass", []string{"-dsn", "redis://foo:bar@localhost/0"}, "foo", "bar", false},
- {"dsn, user override", []string{"-dsn", "redis://foo:bar@localhost/0", "-user", "u"}, "u", "bar", false},
- {"dsn, pass override", []string{"-dsn", "redis://foo:bar@localhost/0", "-pass", "p"}, "foo", "p", false},
+ {"bad dsn", []string{"--dsn", ":localhost/0"}, "", "", true},
+ {"dsn, no info", []string{"--dsn", "redis://localhost/0"}, "", "", false},
+ {"dsn, only pass", []string{"--dsn", "redis://pass@localhost/0"}, "", "pass", false},
+ {"dsn, only empty pass", []string{"--dsn", "redis://@localhost/0"}, "", "", false},
+ {"dsn, user+pass", []string{"--dsn", "redis://foo:bar@localhost/0"}, "foo", "bar", false},
+ {"dsn, user override", []string{"--dsn", "redis://foo:bar@localhost/0", "--user", "u"}, "u", "bar", false},
+ {"dsn, pass override", []string{"--dsn", "redis://foo:bar@localhost/0", "--pass", "p"}, "foo", "p", false},
// Expect error because, during tests, stdIn is not a terminal
- {"dsn, empty pass flag", []string{"-dsn", "redis://localhost/0", "-pass", ""}, "", "", true},
+ {"dsn, empty pass flag", []string{"--dsn", "redis://localhost/0", "--pass", ""}, "", "", true},
}
for _, check := range checks {
t.Run(check.name, func(t *testing.T) {
- fs := flag.NewFlagSet("test", flag.ContinueOnError)
+ fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
userFlag := fs.String("user", "", "")
passFlag := fs.String("pass", "", "")
dsnFlag := fs.String("dsn", "", "")
@@ -86,7 +89,7 @@ func TestGetCredentials(t *testing.T) {
if err != nil && !check.expError {
t.Fatalf("Unexpected error: %v", err)
}
- user, pass, err := getCredentials(fs, io.Discard, *dsnFlag, *userFlag, *passFlag)
+ user, pass, err := getCredentials(fs, io.Discard, redis.DSNValue(*dsnFlag), redis.UserValue(*userFlag), redis.PassValue(*passFlag))
// Redis interprets redis://foo@host as having foo for password, not for user,
// unlike normal URL auth parsing.
if err != nil && !check.expError {
@@ -104,7 +107,8 @@ func TestGetCredentials(t *testing.T) {
type MockConn struct {
RequirePass bool
- User, Pass string
+ User redis.UserValue
+ Pass redis.PassValue
}
func (mc *MockConn) Close() error { return nil }
@@ -149,12 +153,14 @@ func (mc *MockConn) Receive() (reply interface{}, err error) { return nil, nil }
func TestAuthenticate(t *testing.T) {
checks := [...]struct {
- name string
- includeUser bool
- user, pass string
- requirePass bool // redis.conf requirepass = true
- aclUser, aclPass string
- expErr bool
+ name string
+ includeUser bool
+ user redis.UserValue
+ pass redis.PassValue
+ requirePass bool // redis.conf requirepass = true
+ aclUser redis.UserValue
+ aclPass redis.PassValue
+ expErr bool
}{
{"requirepass, only pass", false, "", "pass", true, "", "pass", false},
{"requirepass, no pass", false, "", "", true, "", "pass", true},
@@ -169,7 +175,7 @@ func TestAuthenticate(t *testing.T) {
User: check.aclUser,
Pass: check.aclPass,
}
- err := authenticate(conn, check.includeUser, check.user, check.pass)
+ err := redis.Authenticate(conn, check.includeUser, check.user, check.pass)
if err != nil && !check.expErr {
t.Fatalf("unexpected error: %v", err)
} else if err == nil && check.expErr {
diff --git a/cmd/root.go b/cmd/root.go
new file mode 100644
index 0000000..d9ab300
--- /dev/null
+++ b/cmd/root.go
@@ -0,0 +1,89 @@
+package cmd
+
+import (
+ "fmt"
+ "os"
+ "path"
+
+ "github.com/spf13/cobra"
+
+ "github.com/mitchellh/go-homedir"
+ "github.com/spf13/viper"
+
+ "github.com/fgm/drupal_redis_stats/redis"
+)
+
+var (
+ cfgFile string
+ dsn redis.DSNValue = "redis://localhost:6379/0"
+ jsonOutput redis.JSONValue
+ quiet redis.QuietValue
+ user redis.UserValue
+ pass redis.PassValue
+)
+
+// rootCmd represents the base command when called without any subcommands
+var rootCmd = &cobra.Command{
+ Use: path.Base(os.Args[0]),
+ Short: "Drupal Redis cache control",
+ Long: `Commands helping with the use of the Redis cache in Drupal 8/9.`,
+ RunE: statsRun,
+ PersistentPreRunE: rootPreRunE,
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+ rootInit()
+ statsInit()
+ if err := rootCmd.Execute(); err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+}
+
+func rootInit() {
+ cobra.OnInitialize(initConfig)
+ pf := rootCmd.PersistentFlags()
+ pf.StringVar(&cfgFile, "config", "", "config file (default is $HOME/.drupal_redis_stats.yaml)")
+ pf.Var(&dsn, "dsn", "Can include user and password, per https://www.iana.org/assignments/uri-schemes/prov/redis")
+ pf.VarP(&user, "user", "u", "user name if Redis is configured with ACL. Overrides the DSN user.")
+ pf.VarP(&pass, "pass", "p", "Password. If it is empty it's asked from the tty. Overrides the DSN password.")
+ pf.VarP(&jsonOutput, "json", "j", "Use JSON output.")
+ pf.VarP(&quiet, "quiet", "q", "Do not display scan progress")
+}
+
+// initConfig reads in config file and ENV variables if set.
+func initConfig() {
+ if cfgFile != "" {
+ // Use config file from the flag.
+ viper.SetConfigFile(cfgFile)
+ } else {
+ // Find home directory.
+ home, err := homedir.Dir()
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+
+ // Search config in home directory with name ".drupal_redis_stats" (without extension).
+ viper.AddConfigPath(home)
+ viper.SetConfigName(".drupal_redis_stats")
+ }
+
+ viper.AutomaticEnv() // read in environment variables that match
+
+ // If a config file is found, read it in.
+ if err := viper.ReadInConfig(); err == nil {
+ fmt.Println("Using config file:", viper.ConfigFileUsed())
+ }
+}
+
+func rootPreRunE(cmd *cobra.Command, _ []string) error {
+ var err error
+ user, pass, err = getCredentials(cmd.Flags(), os.Stdout, dsn, user, pass)
+ if err != nil {
+ return fmt.Errorf("failed obtaining user/pass: %v", err)
+ }
+ return nil
+}
diff --git a/cmd/stats.go b/cmd/stats.go
new file mode 100644
index 0000000..aa58cad
--- /dev/null
+++ b/cmd/stats.go
@@ -0,0 +1,49 @@
+package cmd
+
+import (
+ "io"
+ "io/ioutil"
+ "os"
+
+ "github.com/spf13/cobra"
+
+ "github.com/fgm/drupal_redis_stats/output"
+ "github.com/fgm/drupal_redis_stats/redis"
+)
+
+// statsCmd represents the stats command
+var statsCmd = &cobra.Command{
+ Use: "stats",
+ Short: "show cache stats",
+ Long: `Build a list of statistics per Drupal cache bin.`,
+ RunE: statsRun,
+}
+
+func getVerboseWriter(quiet redis.QuietValue) io.Writer {
+ if quiet {
+ return ioutil.Discard
+ }
+ return os.Stderr
+}
+func statsInit() {
+ rootCmd.AddCommand(statsCmd)
+}
+
+func statsRun(cmd *cobra.Command, args []string) error {
+ verboseWriter := getVerboseWriter(quiet)
+ scanner, err := wireStatsScanner(cmd.Flags(), dsn, user, pass)
+ if err != nil {
+ return err
+ }
+ defer scanner.Close()
+
+ s, err := scanner.Scan(verboseWriter, 0)
+ if err != nil {
+ return err
+ }
+
+ if jsonOutput {
+ return output.JSON(os.Stdout, s)
+ }
+ return output.Text(os.Stdout, s)
+}
diff --git a/cmd/stats_test.go b/cmd/stats_test.go
new file mode 100644
index 0000000..23b80cc
--- /dev/null
+++ b/cmd/stats_test.go
@@ -0,0 +1,28 @@
+package cmd
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/fgm/drupal_redis_stats/redis"
+)
+
+func TestGetVerboseWriter(t *testing.T) {
+ checks := [...]struct {
+ name string
+ quiet redis.QuietValue
+ expectedType string
+ }{
+ {"true", true, "io.discard"},
+ {"false", false, "*os.File"},
+ }
+ for _, check := range checks {
+ t.Run(check.name, func(t *testing.T) {
+ actual := getVerboseWriter(check.quiet)
+ actualType := fmt.Sprintf("%T", actual)
+ if actualType != check.expectedType {
+ t.Errorf("Expected %s, got %s", actualType, check.expectedType)
+ }
+ })
+ }
+}
diff --git a/cmd/wire.go b/cmd/wire.go
new file mode 100644
index 0000000..9d0184b
--- /dev/null
+++ b/cmd/wire.go
@@ -0,0 +1,21 @@
+// +build wireinject
+
+package cmd
+
+import (
+ "github.com/google/wire"
+ "github.com/spf13/pflag"
+
+ "github.com/fgm/drupal_redis_stats/redis"
+ "github.com/fgm/drupal_redis_stats/stats"
+)
+
+func wireAuthConn(fs *pflag.FlagSet, dsn redis.DSNValue, user redis.UserValue, pass redis.PassValue) (*redis.AuthConn, error) {
+ wire.Build(redis.NewAuthConn, redis.NewBaseConn)
+ return &redis.AuthConn{}, nil
+}
+
+func wireStatsScanner(fs *pflag.FlagSet, dsn redis.DSNValue, user redis.UserValue, pass redis.PassValue) (stats.Scanner, error) {
+ wire.Build(stats.NewScanner, redis.NewAuthConn, redis.NewBaseConn)
+ return &stats.RealScanner{}, nil
+}
diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go
new file mode 100644
index 0000000..8de15ab
--- /dev/null
+++ b/cmd/wire_gen.go
@@ -0,0 +1,39 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run github.com/google/wire/cmd/wire
+//+build !wireinject
+
+package cmd
+
+import (
+ "github.com/fgm/drupal_redis_stats/redis"
+ "github.com/fgm/drupal_redis_stats/stats"
+ "github.com/spf13/pflag"
+)
+
+// Injectors from wire.go:
+
+func wireAuthConn(fs *pflag.FlagSet, dsn2 redis.DSNValue, user2 redis.UserValue, pass2 redis.PassValue) (*redis.AuthConn, error) {
+ conn, err := redis.NewBaseConn(dsn2)
+ if err != nil {
+ return nil, err
+ }
+ authConn, err := redis.NewAuthConn(conn, fs, user2, pass2)
+ if err != nil {
+ return nil, err
+ }
+ return authConn, nil
+}
+
+func wireStatsScanner(fs *pflag.FlagSet, dsn2 redis.DSNValue, user2 redis.UserValue, pass2 redis.PassValue) (stats.Scanner, error) {
+ conn, err := redis.NewBaseConn(dsn2)
+ if err != nil {
+ return nil, err
+ }
+ authConn, err := redis.NewAuthConn(conn, fs, user2, pass2)
+ if err != nil {
+ return nil, err
+ }
+ scanner := stats.NewScanner(authConn)
+ return scanner, nil
+}
diff --git a/drupal_redis_stats.go b/drupal_redis_stats.go
deleted file mode 100644
index 0f7b411..0000000
--- a/drupal_redis_stats.go
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
-This simple CLI application scans a Redis database for Drupal 8 cache content.
-
-It then return statistics about that instance, in plain text or JSON.
-*/
-package main
-
-import (
- "flag"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "os"
-
- "github.com/gomodule/redigo/redis"
-
- "github.com/fgm/drupal_redis_stats/output"
- "github.com/fgm/drupal_redis_stats/stats"
-)
-
-func getVerboseWriter(quiet bool) io.Writer {
- if quiet {
- return ioutil.Discard
- }
- return os.Stderr
-}
-
-func isFlagPassed(fs *flag.FlagSet, name string) bool {
- found := false
- fs.Visit(func(f *flag.Flag) {
- if f.Name == name {
- found = true
- }
- })
- return found
-}
-
-// open the Redis connection and authenticate is needed.
-func open(dsn *string, user string, pass string, fs *flag.FlagSet) (redis.Conn, error) {
- var c redis.Conn
- var err error
-
- // Connect to the server (ex: DB #1, default #0).
- if c, err = redis.DialURL(*dsn); err != nil {
- return nil, fmt.Errorf("failed dialing Redis URL: %w", err)
- }
-
- if user != "" || pass != "" {
- if err = authenticate(c, isFlagPassed(fs, "user"), user, pass); err != nil {
- return nil, err
- }
- }
- return c, err
-}
-
-func main() {
- var user, pass string
- var err error
- var quiet bool
-
- fs := flag.NewFlagSet("cli", flag.ContinueOnError)
- flagUser := fs.String("user", "", "user name if Redis is configured with ACL. Overrides the DSN user.")
- flagPass := fs.String("pass", "", "Password. If it is empty it's asked from the tty. Overrides the DSN password.")
- dsn := fs.String("dsn", "redis://localhost:6379/0", "Can include user and password, per https://www.iana.org/assignments/uri-schemes/prov/redis")
- jsonOutput := fs.Bool("json", false, "Use JSON output.")
- fs.BoolVar(&quiet, "q", false, "Do not display scan progress")
- if err := fs.Parse(os.Args[1:]); err != nil {
- log.Fatalf("failed parsing flags: %v", err)
- }
-
- verboseWriter := getVerboseWriter(quiet)
-
- if user, pass, err = getCredentials(fs, os.Stdout, *dsn, *flagUser, *flagPass); err != nil {
- log.Fatalf("failed obtaining user/pass: %v", err)
- }
-
- c, err := open(dsn, user, pass, fs)
- if err != nil {
- log.Fatal(err)
- }
- defer c.Close()
-
- var stats stats.CacheStats
- if err = stats.Scan(c, 0, verboseWriter); err != nil {
- log.Fatalf("failed SCAN: %v", err)
- }
-
- if *jsonOutput {
- _ = output.JSON(os.Stderr, &stats)
- } else {
- output.Text(os.Stdout, &stats)
- }
-}
diff --git a/go.mod b/go.mod
index d4d709e..4a8bcac 100644
--- a/go.mod
+++ b/go.mod
@@ -1,11 +1,35 @@
module github.com/fgm/drupal_redis_stats
-go 1.21
+go 1.22
require (
github.com/gomodule/redigo v1.8.9
+ github.com/google/wire v0.6.0
+ github.com/mitchellh/go-homedir v1.1.0
github.com/morikuni/aec v1.0.0
+ github.com/spf13/cobra v1.8.0
+ github.com/spf13/pflag v1.0.5
+ github.com/spf13/viper v1.18.2
golang.org/x/term v0.17.0
)
-require golang.org/x/sys v0.17.0 // indirect
+require (
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/hashicorp/hcl v1.0.0 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/magiconair/properties v1.8.7 // indirect
+ github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/pelletier/go-toml/v2 v2.1.1 // indirect
+ github.com/sagikazarmark/locafero v0.4.0 // indirect
+ github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+ github.com/sourcegraph/conc v0.3.0 // indirect
+ github.com/spf13/afero v1.11.0 // indirect
+ github.com/spf13/cast v1.6.0 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
+ golang.org/x/sys v0.17.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
index 8ee7c7c..3915820 100644
--- a/go.sum
+++ b/go.sum
@@ -1,18 +1,134 @@
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
+github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
+github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
+github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
+github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
+github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
+github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
+github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
+github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
+github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
+golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
+golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..5a86849
--- /dev/null
+++ b/main.go
@@ -0,0 +1,15 @@
+/*
+This simple CLI application scans a Redis database for Drupal 8/9 cache content.
+
+It then return statistics about that instance, in plain text or JSON, and allows
+clearing that cache.
+*/
+package main
+
+import (
+ "github.com/fgm/drupal_redis_stats/cmd"
+)
+
+func main() {
+ cmd.Execute()
+}
diff --git a/main_test.go b/main_test.go
deleted file mode 100644
index 88f7248..0000000
--- a/main_test.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package main
-
-import (
- "flag"
- "fmt"
- "testing"
-)
-
-func TestIsFlagPassed(t *testing.T) {
- const name = "f1"
-
- checks := [...]struct {
- name string
- args []string
- expected bool
- }{
- {"no, nil", nil, false},
- {"no, no flags", []string{}, false},
- {"no, other flags", []string{"-f2", "v2"}, false},
- {"yes, no defaultValue", []string{"-f1", ""}, true},
- {"yes, other value", []string{"-f1", "v11"}, true},
- }
- for _, check := range checks {
- t.Run(check.name, func(t *testing.T) {
- fs := flag.NewFlagSet("test", flag.ContinueOnError)
- fs.String(name, "v1", "")
- fs.String("f2", "v2", "")
- if err := fs.Parse(check.args); err != nil {
- t.Fatalf("failed parsing: %v", err)
- }
- actual := isFlagPassed(fs, name)
- if actual != check.expected {
- t.Errorf("got %t, expected %t", actual, check.expected)
- }
- })
- }
-}
-
-func TestGetVerboseWriter(t *testing.T) {
- checks := [...]struct {
- name string
- quiet bool
- expectedType string
- }{
- {"true", true, "io.discard"},
- {"false", false, "*os.File"},
- }
- for _, check := range checks {
- t.Run(check.name, func(t *testing.T) {
- actual := getVerboseWriter(check.quiet)
- actualType := fmt.Sprintf("%T", actual)
- if actualType != check.expectedType {
- t.Errorf("Expected %s, got %s", actualType, check.expectedType)
- }
- })
- }
-}
diff --git a/output/output.go b/output/output.go
index be87ccc..be266ed 100644
--- a/output/output.go
+++ b/output/output.go
@@ -2,7 +2,7 @@
package output
import (
- _ "embed"
+ _ "embed" // Imported for templates embedding.
"encoding/json"
"errors"
"fmt"
@@ -50,11 +50,14 @@ func compileTemplates() (*template.Template, error) {
}
// Text outputs statistics in text format for CLI usage.
-func Text(w io.Writer, cs *stats.CacheStats) {
+func Text(w io.Writer, cs *stats.CacheStats) error {
if cs == nil {
- panic(errors.New("unexpected nil stats"))
+ return errors.New("unexpected nil stats")
+ }
+ tpl, err := compileTemplates()
+ if err != nil {
+ return err
}
- tpl, _ := compileTemplates()
const binsHeader = "Bin"
const keysHeader = "Keys"
@@ -71,9 +74,9 @@ func Text(w io.Writer, cs *stats.CacheStats) {
SizeLen: int(math.Max(float64(cs.TotalSizeLength()), float64(len(sizeHeader)))),
}
- err := tpl.Execute(w, data)
+ err = tpl.Execute(w, data)
if err != nil {
- // No failure expected for any data, so let's panic.
- panic(err)
+ return err
}
+ return nil
}
diff --git a/output/output_test.go b/output/output_test.go
index cac2619..9535f64 100644
--- a/output/output_test.go
+++ b/output/output_test.go
@@ -68,27 +68,15 @@ func TestTextSadNil(t *testing.T) {
w := strings.Builder{}
var s *stats.CacheStats
- recovered := func() (r bool) {
- defer func() {
- if panicked := recover(); panicked != nil {
- r = true
- }
- }()
- output.Text(&w, s)
- return r
- }
- if !recovered() {
- t.Errorf("test did not panic on nil stats")
+ if err :=output.Text(&w, s); err == nil {
+ t.Errorf("test did not error on nil stats")
}
}
func TestTextSadWriter(t *testing.T) {
- defer func() {
- if err := recover(); err == nil {
- t.Error("calling Text() did not cause panic")
- }
- }()
- output.Text(ErrorWriter(0), &stats.CacheStats{})
+ if err := output.Text(ErrorWriter(0), &stats.CacheStats{}); err == nil {
+ t.Error("unexpected success")
+ }
}
func TestText(t *testing.T) {
diff --git a/redis/redis.go b/redis/redis.go
new file mode 100644
index 0000000..3945901
--- /dev/null
+++ b/redis/redis.go
@@ -0,0 +1,47 @@
+package redis
+
+import (
+ "fmt"
+
+ "github.com/gomodule/redigo/redis"
+ "github.com/spf13/pflag"
+)
+
+// AuthConn is a redis.Conn which is is automatically authenticated if needed.
+type AuthConn struct {
+ redis.Conn
+}
+
+// NewAuthConn authenticates the passed redis.Conn if needed.
+func NewAuthConn(baseConn redis.Conn, fs *pflag.FlagSet, user UserValue, pass PassValue) (*AuthConn, error) {
+ ac := AuthConn{Conn: baseConn}
+ var err error
+
+ if user != "" || pass != "" {
+ if err = Authenticate(ac, IsFlagPassed(fs, "user"), user, pass); err != nil {
+ return nil, err
+ }
+ }
+ return &ac, err
+}
+
+// NewBaseConn provides a standard Redis connection.
+func NewBaseConn(dsn DSNValue) (redis.Conn, error) {
+ return redis.DialURL(string(dsn))
+}
+
+// Authenticate attempts authentication on an already opened standard Redis connection.
+func Authenticate(c redis.Conn, includeUser bool, user UserValue, pass PassValue) error {
+ var err error
+
+ if includeUser {
+ _, err = c.Do("AUTH", user, pass)
+ } else {
+ _, err = c.Do("AUTH", pass)
+ }
+ if err != nil {
+ return fmt.Errorf("failed AUTH: %w", err)
+ }
+
+ return nil
+}
diff --git a/redis/redis_test.go b/redis/redis_test.go
new file mode 100644
index 0000000..0539397
--- /dev/null
+++ b/redis/redis_test.go
@@ -0,0 +1,39 @@
+package redis_test
+
+import (
+ "testing"
+
+ "github.com/spf13/pflag"
+
+ "github.com/fgm/drupal_redis_stats/redis"
+)
+
+func TestIsFlagPassed(t *testing.T) {
+ const name = "f1"
+
+ checks := [...]struct {
+ name string
+ args []string
+ expected bool
+ }{
+ {"no, nil", nil, false},
+ {"no, no flags", []string{}, false},
+ {"no, other flags", []string{"--f2", "v2"}, false},
+ {"yes, no defaultValue", []string{"--f1", ""}, true},
+ {"yes, other value", []string{"--f1", "v11"}, true},
+ }
+ for _, check := range checks {
+ t.Run(check.name, func(t *testing.T) {
+ fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
+ fs.StringP(name, "", "v1", "")
+ fs.StringP("f2", "", "v2", "")
+ if err := fs.Parse(check.args); err != nil {
+ t.Fatalf("failed parsing: %v", err)
+ }
+ actual := redis.IsFlagPassed(fs, name)
+ if actual != check.expected {
+ t.Errorf("got %t, expected %t", actual, check.expected)
+ }
+ })
+ }
+}
diff --git a/redis/types.go b/redis/types.go
new file mode 100644
index 0000000..5d38591
--- /dev/null
+++ b/redis/types.go
@@ -0,0 +1,84 @@
+package redis
+
+import (
+ "strconv"
+
+ "github.com/spf13/pflag"
+)
+
+type (
+ // DSNValue is a wire-compatible string for DSN flags
+ DSNValue string
+ // JSONValue is a wire-compatible boolean for jsonOutput
+ JSONValue bool
+ // PassValue is a wire-compatible string for passwords
+ PassValue string
+ // QuietValue is a wire-compatible boolean for quiet
+ QuietValue bool
+ // UserValue is a wire-compatible string for user names
+ UserValue string
+)
+
+// String is part of the pflag.Value implementation
+func (d *DSNValue) String() string { return string(*d) }
+
+// Set is part of the pflag.Value implementation
+func (d *DSNValue) Set(s string) error { *d = DSNValue(s); return nil }
+
+// Type is part of the pflag.Value implementation
+func (d *DSNValue) Type() string { return "string" }
+
+// String is part of the pflag.Value implementation
+func (d *PassValue) String() string { return string(*d) }
+
+// Set is part of the pflag.Value implementation
+func (d *PassValue) Set(s string) error { *d = PassValue(s); return nil }
+
+// Type is part of the pflag.Value implementation
+func (d *PassValue) Type() string { return "string" }
+
+// String is part of the pflag.Value implementation
+func (d *UserValue) String() string { return string(*d) }
+
+// Set is part of the pflag.Value implementation
+func (d *UserValue) Set(s string) error { *d = UserValue(s); return nil }
+
+// Type is part of the pflag.Value implementation
+func (d *UserValue) Type() string { return "string" }
+
+// String is part of the pflag.Value implementation
+func (d *JSONValue) String() string { return strconv.FormatBool(bool(*d)) }
+
+// Set is part of the pflag.Value implementation
+func (d *JSONValue) Set(s string) error {
+ b, err := strconv.ParseBool(s)
+ *d = JSONValue(b)
+ return err
+}
+
+// Type is part of the pflag.Value implementation
+func (d *JSONValue) Type() string { return "bool" }
+
+// String is part of the pflag.Value implementation
+func (d *QuietValue) String() string { return strconv.FormatBool(bool(*d)) }
+
+// Set is part of the pflag.Value implementation
+func (d *QuietValue) Set(s string) error {
+ b, err := strconv.ParseBool(s)
+ *d = QuietValue(b)
+ return err
+}
+
+// Type is part of the pflag.Value implementation
+func (d *QuietValue) Type() string { return "bool" }
+
+// IsFlagPassed reports whether or not a named flag has been passed.
+func IsFlagPassed(fs *pflag.FlagSet, name string) bool {
+ found := false
+ fs.Visit(func(f *pflag.Flag) {
+ if f.Name == name {
+ found = true
+ }
+ })
+ return found
+}
diff --git a/redis/types_test.go b/redis/types_test.go
new file mode 100644
index 0000000..82e667d
--- /dev/null
+++ b/redis/types_test.go
@@ -0,0 +1,47 @@
+package redis_test
+
+import (
+ "testing"
+
+ "github.com/fgm/drupal_redis_stats/redis"
+)
+
+func TestDSNValue(t *testing.T) {
+ var v redis.DSNValue
+ v.Set(v.Type())
+ if v.String() != "string" {
+ t.Errorf("Expected string got %s", v.String())
+ }
+}
+
+func TestJSONValue(t *testing.T) {
+ var v redis.JSONValue
+ v.Set(v.Type())
+ if v.String() != "false" { // "bool" is not trueish
+ t.Errorf("Expected false got %s", v.String())
+ }
+}
+
+func TestPassValue(t *testing.T) {
+ var v redis.PassValue
+ v.Set(v.Type())
+ if v.String() != "string" {
+ t.Errorf("Expected string got %s", v.String())
+ }
+}
+
+func TestQuietValue(t *testing.T) {
+ var v redis.QuietValue
+ v.Set(v.Type())
+ if v.String() != "false" { // "bool" is not trueish
+ t.Errorf("Expected string got %s", v.String())
+ }
+}
+
+func TestUserValue(t *testing.T) {
+ var v redis.UserValue
+ v.Set(v.Type())
+ if v.String() != "string" {
+ t.Errorf("Expected string got %s", v.String())
+ }
+}
diff --git a/stats/scanner.go b/stats/scanner.go
new file mode 100644
index 0000000..5553a99
--- /dev/null
+++ b/stats/scanner.go
@@ -0,0 +1,98 @@
+package stats
+
+import (
+ "fmt"
+ "io"
+ "regexp"
+
+ "github.com/gomodule/redigo/redis"
+
+ redis2 "github.com/fgm/drupal_redis_stats/redis"
+ "github.com/fgm/drupal_redis_stats/stats/progress"
+)
+
+// NewScanner builds a Scanner
+func NewScanner(conn *redis2.AuthConn) Scanner {
+ return &RealScanner{Conn: conn}
+}
+
+// RealScanner is a concrete Scanner implementation.
+type RealScanner struct {
+ redis.Conn
+ *CacheStats
+}
+
+// Scanner describes a service providing statistics for Drupal cache bins in Redis.
+type Scanner interface {
+ Scan(w io.Writer, maxPasses uint32) (*CacheStats, error)
+ io.Closer
+}
+
+/*
+Scan examines the active database for keys matching the Drupal cache bin format.
+
+ - c is the established connection to Redis on which to perform the Scan.
+ - maxPasses allows limiting the number of Redis SCAN steps. Use 0 for no limit.
+ - writer is a logging output (think os.Stderr), not the main output.
+*/
+func (s *RealScanner) Scan(w io.Writer, maxPasses uint32) (*CacheStats, error) {
+ s.CacheStats = &CacheStats{
+ Stats: map[string]BinStats{},
+ }
+
+ dbSize, err := redis.Uint64(s.Do("DBSIZE"))
+ if err != nil {
+ return nil, err
+ }
+ s.TotalKeys = uint32(dbSize) // Cannot be >= 2^32 in Redis anyway.
+ pb := progress.MakeProgressBar(80, s.TotalKeys)
+ var passes uint32 // The number of performed SCAN passes.
+ var seen float64
+ var iterator int // Type chosen by Redigo
+ var keys []string // The keys returned by a single SCAN pass.
+ for {
+ passes++
+ // Run one Scan pass with the current iterator position.
+ arr, err := redis.Values(s.Do("SCAN", iterator, "MATCH", "drupal.redis.*"))
+ if err != nil {
+ return nil, err
+ }
+ // Parse Scan pass results.
+ iterator, _ = redis.Int(arr[0], nil)
+ keys, _ = redis.Strings(arr[1], nil)
+ seen += float64(len(keys))
+ _, _ = fmt.Fprint(w, pb.Render(seen))
+ err = s.indexKeys(keys)
+ if err != nil {
+ return nil, err
+ }
+ // When iteration is done, the returned iterator will be 0.
+ if iterator == 0 || (maxPasses != 0 && passes >= maxPasses) {
+ break
+ }
+ }
+ _, _ = fmt.Fprint(w, pb.Remove())
+ return s.CacheStats, nil
+}
+
+// indexKeys assumes cs.Stats is already initialized to a non-nil value.
+func (s *RealScanner) indexKeys(keys []string) error {
+ const reString = `drupal\.redis\.[-.\d\w]+:([\w]+):(.*)`
+
+ var re = regexp.MustCompile(reString)
+
+ for _, key := range keys {
+ sl := re.FindStringSubmatch(key)
+ if sl == nil {
+ return fmt.Errorf("unexpected non-matching key: %s", key)
+ }
+ bin := sl[1]
+ if _, ok := s.Stats[bin]; !ok {
+ s.Stats[bin] = BinStats{}
+ }
+ binStats := s.Stats[bin]
+ binStats.addEntry(s.Conn, key)
+ s.Stats[bin] = binStats
+ }
+ return nil
+}
diff --git a/stats/stats.go b/stats/stats.go
index 2d67112..350e9a8 100644
--- a/stats/stats.go
+++ b/stats/stats.go
@@ -5,13 +5,9 @@ package stats
import (
"fmt"
- "io"
- "regexp"
"strconv"
"github.com/gomodule/redigo/redis"
-
- "github.com/fgm/drupal_redis_stats/stats/progress"
)
/*
@@ -49,28 +45,6 @@ type CacheStats struct {
Stats map[string]BinStats
}
-// indexKeys assumes cs.Stats is already initialized to a non-nil value.
-func (cs *CacheStats) indexKeys(c redis.Conn, keys []string) error {
- const reString = `drupal\.redis\.[-.\d\w]+:([\w]+):(.*)`
-
- var re = regexp.MustCompile(reString)
-
- for _, key := range keys {
- sl := re.FindStringSubmatch(key)
- if sl == nil {
- return fmt.Errorf("unexpected non-matching key: %s", key)
- }
- bin := sl[1]
- if _, ok := cs.Stats[bin]; !ok {
- cs.Stats[bin] = BinStats{}
- }
- binStats := cs.Stats[bin]
- binStats.addEntry(c, key)
- cs.Stats[bin] = binStats
- }
- return nil
-}
-
/*
MaxBinNameLength returns the length in runes of the longest bin name.
*/
@@ -85,53 +59,6 @@ func (cs CacheStats) MaxBinNameLength() int {
return max
}
-/*
-Scan examines the active database for keys matching the Drupal cache bin format.
-
- - c is the established connection to Redis on which to perform the Scan.
- - maxPasses allows limiting the number of Redis SCAN steps. Use 0 for no limit.
- - writer is a logging output (think os.Stderr), not the main output.
-*/
-func (cs *CacheStats) Scan(c redis.Conn, maxPasses uint32, w io.Writer) error {
- if cs.Stats == nil {
- cs.Stats = map[string]BinStats{}
- }
-
- dbSize, err := redis.Uint64(c.Do("DBSIZE"))
- if err != nil {
- return err
- }
- cs.TotalKeys = uint32(dbSize) // Cannot be >= 2^32 in Redis anyway.
- pb := progress.MakeProgressBar(80, cs.TotalKeys)
- var passes uint32 // The number of performed SCAN passes.
- var seen float64
- var iterator int // Type chosen by Redigo
- var keys []string // The keys returned by a single SCAN pass.
- for {
- passes++
- // Run one Scan pass with the current iterator position.
- arr, err := redis.Values(c.Do("SCAN", iterator, "MATCH", "drupal.redis.*"))
- if err != nil {
- return err
- }
- // Parse Scan pass results.
- iterator, _ = redis.Int(arr[0], nil)
- keys, _ = redis.Strings(arr[1], nil)
- seen += float64(len(keys))
- _, _ = fmt.Fprint(w, pb.Render(seen))
- err = cs.indexKeys(c, keys)
- if err != nil {
- return err
- }
- // When iteration is done, the returned iterator will be 0.
- if iterator == 0 || (maxPasses != 0 && passes >= maxPasses) {
- break
- }
- }
- _, _ = fmt.Fprint(w, pb.Remove())
- return nil
-}
-
/*
ItemCountLength returns the length in runes of the total number of keys.