diff --git a/internal/app/app.go b/internal/app/app.go index c470ed7..ec664ab 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -207,6 +207,8 @@ type FileTracker interface { IsUploaded(file string) bool UnmarkAsUploaded(file string) error Close() error + + Destroy() error } // TokenManager represents a service to keep and read secrets (like passwords, tokens...) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 68eec29..6d4ded3 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -6,6 +6,7 @@ import ( "github.com/gphotosuploader/gphotos-uploader-cli/internal/cli/flags" "github.com/gphotosuploader/gphotos-uploader-cli/internal/cli/list" "github.com/gphotosuploader/gphotos-uploader-cli/internal/cli/push" + "github.com/gphotosuploader/gphotos-uploader-cli/internal/cli/reset" "github.com/gphotosuploader/gphotos-uploader-cli/internal/cli/version" "github.com/gphotosuploader/gphotos-uploader-cli/internal/log" "github.com/mgutz/ansi" @@ -64,6 +65,7 @@ func createCliCommandTree(cmd *cobra.Command) { cmd.AddCommand(push.NewCommand(globalFlags)) cmd.AddCommand(auth.NewCommand(globalFlags)) cmd.AddCommand(list.NewCommand(globalFlags)) + cmd.AddCommand(reset.NewCommand(globalFlags)) // TODO: Set flags here instead of passing globalFlags to all commands. // See: https://github.com/arduino/arduino-cli/blob/master/internal/cli/cli.go diff --git a/internal/cli/reset/reset.go b/internal/cli/reset/reset.go new file mode 100644 index 0000000..421a106 --- /dev/null +++ b/internal/cli/reset/reset.go @@ -0,0 +1,17 @@ +package reset + +import ( + "github.com/gphotosuploader/gphotos-uploader-cli/internal/cli/flags" + "github.com/spf13/cobra" +) + +func NewCommand(globalFlags *flags.GlobalFlags) *cobra.Command { + resetCommand := &cobra.Command{ + Use: "reset", + Short: "Reset internal databases", + } + + resetCommand.AddCommand(initFileTrackerCommand(globalFlags)) + + return resetCommand +} diff --git a/internal/cli/reset/reset_filetracker.go b/internal/cli/reset/reset_filetracker.go new file mode 100644 index 0000000..76fd83c --- /dev/null +++ b/internal/cli/reset/reset_filetracker.go @@ -0,0 +1,71 @@ +package reset + +import ( + "context" + "github.com/gphotosuploader/gphotos-uploader-cli/internal/app" + "github.com/gphotosuploader/gphotos-uploader-cli/internal/cli/flags" + "github.com/gphotosuploader/gphotos-uploader-cli/internal/feedback" + "github.com/spf13/cobra" +) + +// FileTrackerCommandOptions contains the input to the 'reset file-tracker' command. +type FileTrackerCommandOptions struct { + *flags.GlobalFlags + + Force bool +} + +func initFileTrackerCommand(globalFlags *flags.GlobalFlags) *cobra.Command { + o := &FileTrackerCommandOptions{ + GlobalFlags: globalFlags, + + Force: false, + } + + command := &cobra.Command{ + Use: "file-tracker", + Short: "Reset the already uploaded files database", + Long: `Reset the internal database which keep track of the already uploaded files.`, + Args: cobra.NoArgs, + RunE: o.Run, + } + + command.Flags().BoolVar(&o.Force, "force", false, "Force the deletion without asking.") + + return command +} + +func (o *FileTrackerCommandOptions) Run(cobraCmd *cobra.Command, args []string) error { + ctx := context.Background() + cli, err := app.Start(ctx, o.CfgDir) + if err != nil { + return err + } + defer func() { + _ = cli.Stop() + }() + + cli.Logger.Debug("Removing the File Tracker database...") + + // If the force flag is not user, ask for user confirmation + if !o.Force { + userConfirmation, err := feedback.YesNoPrompt("Do you want to reset the already uploaded file tracker?", false) + if err != nil { + return err + } + + if !userConfirmation { + cobraCmd.Println("User aborted the removal of the File Tracker") + return nil + } + } + + err = cli.FileTracker.Destroy() + if err != nil { + return err + } + + cobraCmd.Println("File Tracker was reset") + + return nil +} diff --git a/internal/datastore/filetracker/filetracker.go b/internal/datastore/filetracker/filetracker.go index 96cdde3..2cf0e85 100644 --- a/internal/datastore/filetracker/filetracker.go +++ b/internal/datastore/filetracker/filetracker.go @@ -29,6 +29,7 @@ type FileRepository interface { Put(key string, item TrackedFile) error Delete(key string) error Close() error + Destroy() error } // New returns a FileTracker using specified repo. @@ -110,3 +111,8 @@ func (ft FileTracker) UnmarkAsUploaded(file string) error { func (ft FileTracker) Close() error { return ft.repo.Close() } + +// Destroy completely remove an existing FileTracker database. +func (ft FileTracker) Destroy() error { + return ft.repo.Destroy() +} diff --git a/internal/datastore/filetracker/filetracker_test.go b/internal/datastore/filetracker/filetracker_test.go index 0b603a4..e7f7372 100644 --- a/internal/datastore/filetracker/filetracker_test.go +++ b/internal/datastore/filetracker/filetracker_test.go @@ -135,6 +135,10 @@ func (m mockedRepository) Close() error { return nil } +func (m mockedRepository) Destroy() error { + return nil +} + type mockedHasher struct { hash string } diff --git a/internal/datastore/filetracker/leveldb_repository.go b/internal/datastore/filetracker/leveldb_repository.go index c029dc6..8b73d37 100644 --- a/internal/datastore/filetracker/leveldb_repository.go +++ b/internal/datastore/filetracker/leveldb_repository.go @@ -3,6 +3,7 @@ package filetracker import ( "github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb/opt" + "os" ) // DB represents a LevelDB database. @@ -15,19 +16,21 @@ type DB interface { // LevelDBRepository implements a FileRepository using LevelDB. type LevelDBRepository struct { - DB DB + DB DB + path string } // NewLevelDBRepository creates a repository using LevelDB package. -func NewLevelDBRepository(filename string) (*LevelDBRepository, error) { - ft, err := leveldb.OpenFile(filename, nil) +func NewLevelDBRepository(path string) (*LevelDBRepository, error) { + ft, err := leveldb.OpenFile(path, nil) return &LevelDBRepository{ - DB: ft, + DB: ft, + path: path, }, err } // Get returns the item specified by key. It returns ErrItemNotFound if the -// DB does not contains the key. +// DB does not contain the key. func (r LevelDBRepository) Get(key string) (TrackedFile, bool) { val, err := r.DB.Get([]byte(key), nil) if err != nil { @@ -50,3 +53,9 @@ func (r LevelDBRepository) Delete(key string) error { func (r LevelDBRepository) Close() error { return r.DB.Close() } + +// Destroy completely remove an existing LevelDB database directory. +func (r LevelDBRepository) Destroy() error { + _ = r.DB.Close() + return os.RemoveAll(r.path) +} diff --git a/internal/datastore/upload_tracker/leveldb.go b/internal/datastore/upload_tracker/leveldb.go index 2425796..ebee53e 100644 --- a/internal/datastore/upload_tracker/leveldb.go +++ b/internal/datastore/upload_tracker/leveldb.go @@ -3,10 +3,12 @@ package upload_tracker import ( "github.com/syndtr/goleveldb/leveldb" + "os" ) type LevelDBStore struct { - db *leveldb.DB + db *leveldb.DB + path string } // NewStore create a new Store implemented by LevelDB @@ -16,7 +18,10 @@ func NewStore(path string) (*LevelDBStore, error) { return nil, err } - s := &LevelDBStore{db: db} + s := &LevelDBStore{ + db: db, + path: path, + } return s, err } @@ -42,3 +47,9 @@ func (s *LevelDBStore) Delete(key string) { func (s *LevelDBStore) Close() { _ = s.db.Close() } + +// Destroy completely remove an existing LevelDB database directory. +func (s *LevelDBStore) Destroy() error { + _ = s.db.Close() + return os.RemoveAll(s.path) +} diff --git a/internal/feedback/terminal.go b/internal/feedback/terminal.go index 8caf6f2..c5cab1d 100644 --- a/internal/feedback/terminal.go +++ b/internal/feedback/terminal.go @@ -6,6 +6,7 @@ import ( "fmt" "golang.org/x/term" "os" + "strings" ) func isTerminal() bool { @@ -32,3 +33,29 @@ func InputUserField(prompt string, secret bool) (string, error) { sc.Scan() return sc.Text(), sc.Err() } + +func YesNoPrompt(prompt string, def bool) (bool, error) { + choices := "Y/n" + if !def { + choices = "y/N" + } + + prompt = fmt.Sprintf("%s (%s)", prompt, choices) + + for { + s, err := InputUserField(prompt, false) + if err != nil { + return def, err + } + if s == "" { + return def, nil + } + s = strings.ToLower(s) + if s == "y" || s == "yes" { + return true, nil + } + if s == "n" || s == "no" { + return false, nil + } + } +}