Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the sync command #211

Merged
merged 3 commits into from
Aug 16, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,30 @@ import (
)

var (
syncCmdDesc = "Sync a list with ultralist.io"
syncCmdExample = "ultralist sync"
syncCmdDesc = "Sync a list with ultralist.io"
syncCmdExample = `ultralist sync
ultralist sync --setup
ultralist sync --unsync
ultralist sync --quiet
`

syncCmdQuiet bool
syncCmdLongDesc = `
setupCmd bool
unsyncCmd bool
syncCmdLongDesc = `Sync a list with Ultralist Pro.

ultralist sync
The Ultralist CLI stores tasks locally. When running sync, it will push
up any local changes to ultralist.io, as well as pulling down any remote changes from ultralist.io.

The sync command has a few uses:
* If you are logged into ultralist.io (via "ultralist auth"), you can sync an existing list using "ultralist sync".
* For a synced list, you can run "ultralist sync" explicitly to receive any changes that may have occurred elsewhere.
ultralist sync --setup
Set up the local list to sync with ultralist.io. Or, pull a list from Ultralist.io to local.

Local changes to a synced list automatically get pushed to ultralist.io.
ultralist sync --unsync
Stop syncing a local list with ultralist.io.

ultralist sync --quiet
Perform a sync without showing output to the screen.

See https://ultralist.io/docs/cli/pro_integration for more info.
`
Expand All @@ -27,11 +41,22 @@ var syncCmd = &cobra.Command{
Long: syncCmdLongDesc,
Short: syncCmdDesc,
Run: func(cmd *cobra.Command, args []string) {
if setupCmd {
ultralist.NewApp().SetupSync()
return
}
if unsyncCmd {
ultralist.NewApp().Unsync()
return
}

ultralist.NewApp().Sync(syncCmdQuiet)
},
}

func init() {
syncCmd.Flags().BoolVarP(&syncCmdQuiet, "quiet", "q", false, "Run without output")
syncCmd.Flags().BoolVarP(&setupCmd, "setup", "", false, "Set up a list to sync with ultralist.io, or pull a remote list to local")
syncCmd.Flags().BoolVarP(&unsyncCmd, "unsync", "", false, "Stop syncing a list with ultralist.io")
rootCmd.AddCommand(syncCmd)
}
168 changes: 99 additions & 69 deletions ultralist/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,63 +55,6 @@ func NewAppWithPrintOptions(unicodeSupport bool, colorSupport bool) *App {
func (a *App) InitializeRepo() {
a.TodoStore.Initialize()
fmt.Println("Repo initialized.")

backend := NewBackend()
eventLogger := &EventLogger{Store: a.TodoStore, CurrentTodoList: a.TodoList}
eventLogger.LoadSyncedLists()

if !backend.CredsFileExists() {
return
}

prompt := promptui.Prompt{
Label: "Do you wish to sync this list with ultralist.io",
IsConfirm: true,
}

result, _ := prompt.Run()
if result != "y" {
return
}

if !backend.CanConnect() {
fmt.Println("I cannot connect to ultralist.io right now.")
return
}

// fetch lists from ultralist.io, or allow user to create a new list
// use the "select_add" example in promptui as a way to do this
type Response struct {
Todolists []TodoList `json:"todolists"`
}

var response *Response

resp := backend.PerformRequest("GET", "/api/v1/todo_lists", []byte{})
json.Unmarshal(resp, &response)

var todolistNames []string
for _, todolist := range response.Todolists {
todolistNames = append(todolistNames, todolist.Name)
}

prompt2 := promptui.SelectWithAdd{
Label: "You can sync with an existing list on ultralist, or create a new list.",
Items: todolistNames,
AddLabel: "New list...",
}

idx, name, _ := prompt2.Run()
if idx == -1 {
eventLogger.CurrentSyncedList.Name = name
} else {
eventLogger.CurrentSyncedList.Name = response.Todolists[idx].Name
eventLogger.CurrentSyncedList.UUID = response.Todolists[idx].UUID
a.TodoList = &response.Todolists[idx]
a.save()
}

eventLogger.WriteSyncedLists()
}

// AddTodo is adding a new todo.
Expand Down Expand Up @@ -360,19 +303,16 @@ func (a *App) GarbageCollect() {

// Sync will sync the todolist with ultralist.io.
func (a *App) Sync(quiet bool) {
a.Load()

if a.EventLogger.CurrentSyncedList.Name == "" {
prompt := promptui.Prompt{
Label: "Give this list a name",
}
backend := NewBackend()
if !backend.CredsFileExists() {
fmt.Println("You're not authenticated with ultralist.io yet. Please run `ultralist auth` first.")
return
}

result, err := prompt.Run()
if err != nil {
fmt.Println("A name is required to sync a list.")
return
}
a.EventLogger.CurrentSyncedList.Name = result
a.Load()
if !a.TodoList.IsSynced {
fmt.Println("This list isn't currently syncing with ultralist.io. Please run `ultralist sync --setup` to set up syncing.")
return
}

var synchronizer *Synchronizer
Expand All @@ -389,6 +329,96 @@ func (a *App) Sync(quiet bool) {
}
}

// SetupSync sets up a todolist to sync with ultralist.io.
func (a *App) SetupSync() {
backend := NewBackend()
if !backend.CredsFileExists() {
fmt.Println("You're not authenticated with ultralist.io yet. Please run `ultralist auth` first.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this error isnt strictly true, it is only checking for if the file exists and could contain bad auth keys.

NewBackend(), already check and loads this file, so would a more correct test be.

    if backend.Credentials == "" {
		fmt.Println("You're not authenticated with ultralist.io yet.  Please run `ultralist auth` first.")
                 return
}

Side note: If you are using JWT (I dont have a UL account so I am guessing) as the authentication, should we also check to see it is valid before trying to use it? or as part of the loadCreds, or a new function checkCreds.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, that's a good catch!

Regarding the JWT, I don't feel strongly about validating, since the request would ultimately fail if they have invalid creds.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did try it with just using backend.Creds, and it didn't seem to work. idk 🤷

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try with a ~/.config/ultralist/cred.json with {}.

Also a blank file will cause a panic when NewBackend() tries to parse it.

I was thinking about turning those panics into a log and non-0 exit.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure there's a scenario where creds.json would have that info in it though.

return
}

if a.TodoStore.LocalTodosFileExists() {
a.Load()

if a.TodoList.IsSynced {
fmt.Println("This list is already sycned with ultralist.io. Use the --unsync flag to stop syncing this list.")
return
}

prompt := promptui.Select{
Label: "You have a todos list in this directory. What would you like to do?",
Items: []string{"Sync my list to ultralist.io", "Pull a list from ultralist.io, replacing the list that's here"},
}
_, result, err := prompt.Run()
if err != nil {
return
}
if strings.HasPrefix(result, "Sync my list") {
prompt := promptui.Prompt{
Label: "Give this list a name",
}

result, err := prompt.Run()
if err != nil {
fmt.Println("A name is required to sync a list.")
return
}
a.EventLogger.CurrentSyncedList.Name = result
a.TodoList.IsSynced = true
a.EventLogger.WriteSyncedLists()
a.Sync(false)
return
}
}
// pull a list from ultralist.io
type Response struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be its me not being a full time golang programmer, but a struct, definition inside of a func seems a little odd (unless you are trying to hide it)

Todolists []TodoList `json:"todolists"`
}

var response *Response

resp := backend.PerformRequest("GET", "/api/v1/todo_lists", []byte{})
json.Unmarshal(resp, &response)

var todolistNames []string
for _, todolist := range response.Todolists {
todolistNames = append(todolistNames, todolist.Name)
}
prompt2 := promptui.Select{
Label: "Choose a list to import",
Items: todolistNames,
}

idx, _, _ := prompt2.Run()
// a.EventLogger.CurrentSyncedList.Name = response.Todolists[idx].Name
// a.EventLogger.CurrentSyncedList.UUID = response.Todolists[idx].UUID
a.TodoList = &response.Todolists[idx]
a.TodoStore.Save(a.TodoList.Data)

a.EventLogger = NewEventLogger(a.TodoList, a.TodoStore)
a.EventLogger.WriteSyncedLists()
}

// Unsync stops a list from syncing with Ultralist.io.
func (a *App) Unsync() {
backend := NewBackend()
if !backend.CredsFileExists() {
fmt.Println("You're not authenticated with ultralist.io yet. Please run `ultralist auth` first.")
return
}

a.Load()

if !a.TodoList.IsSynced {
fmt.Println("This list isn't currently syncing with ultralist.io.")
return
}

a.EventLogger.DeleteCurrentSyncedList()
a.EventLogger.WriteSyncedLists()
fmt.Println("This list will no longer sync with ultralist.io. To set up syncing again, run `ultralist sync --setup`.")
}

// CheckAuth is checking the authentication against ultralist.io.
func (a *App) CheckAuth() {
synchronizer := NewSynchronizer()
Expand Down
11 changes: 11 additions & 0 deletions ultralist/event_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,17 @@ func (e *EventLogger) initializeSyncedList() {
e.CurrentSyncedList = list
}

// DeleteCurrentSyncedList - delete a synced list from the synced_lists.json file
func (e *EventLogger) DeleteCurrentSyncedList() {
var syncedListsWithoutDeleted []*SyncedList
for _, list := range e.SyncedLists {
if list.UUID != e.CurrentSyncedList.UUID {
syncedListsWithoutDeleted = append(syncedListsWithoutDeleted, list)
}
}
e.SyncedLists = syncedListsWithoutDeleted
}

// WriteSyncedLists is writing a synced list.
func (e *EventLogger) WriteSyncedLists() {
data, _ := json.Marshal(e.SyncedLists)
Expand Down
47 changes: 23 additions & 24 deletions ultralist/file_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,45 @@ import (
"os"
)

// the filename to use
const FILENAME = ".todos.json"

// FileStore is the main struct of this file.
type FileStore struct {
FileLocation string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing this breaks tests.

# github.com/ultralist/ultralist/ultralist [github.com/ultralist/ultralist/ultralist.test]
./file_store_test.go:13:22: unknown field 'FileLocation' in struct literal of type FileStore
./file_store_test.go:18:23: unknown field 'FileLocation' in struct literal of type FileStore
FAIL	github.com/ultralist/ultralist/ultralist [build failed]

Copy link
Owner Author

@gammons gammons Aug 16, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch!

Loaded bool
Loaded bool
}

// NewFileStore is creating a new file store.
func NewFileStore() *FileStore {
return &FileStore{FileLocation: "", Loaded: false}
return &FileStore{Loaded: false}
}

// Initialize is initializing a new .todos.json file.
func (f *FileStore) Initialize() {
if f.FileLocation == "" {
f.FileLocation = ".todos.json"
}

_, err := ioutil.ReadFile(f.FileLocation)
if err == nil {
if f.LocalTodosFileExists() {
fmt.Println("It looks like a .todos.json file already exists! Doing nothing.")
os.Exit(0)
}
if err := ioutil.WriteFile(f.FileLocation, []byte("[]"), 0644); err != nil {
if err := ioutil.WriteFile(FILENAME, []byte("[]"), 0644); err != nil {
fmt.Println("Error writing json file", err)
os.Exit(1)
}
}

// Load is loading a .todos.json file.
func (f *FileStore) Load() ([]*Todo, error) {
if f.FileLocation == "" {
f.FileLocation = f.GetLocation()
// Returns if a local .todos.json file exists in the current dir.
func (f *FileStore) LocalTodosFileExists() bool {
dir, _ := os.Getwd()
localrepo := fmt.Sprintf("%s/.todos.json", dir)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would filepath.Join(dir, FILENAME) be more robust?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep it would.

_, err := os.Stat(localrepo)
if err != nil {
return false
}
return true
}

data, err := ioutil.ReadFile(f.FileLocation)
// Load is loading a .todos.json file, either from cwd, or the home directory
func (f *FileStore) Load() ([]*Todo, error) {
data, err := ioutil.ReadFile(f.GetLocation())
if err != nil {
fmt.Println("No todo file found!")
fmt.Println("Initialize a new todo repo by running 'ultralist init'")
Expand Down Expand Up @@ -71,20 +75,15 @@ func (f *FileStore) Save(todos []*Todo) {
}

data, _ := json.Marshal(todos)
if err := ioutil.WriteFile(f.FileLocation, []byte(data), 0644); err != nil {
if err := ioutil.WriteFile(FILENAME, []byte(data), 0644); err != nil {
fmt.Println("Error writing json file", err)
}
}

// GetLocation is returning the location of the .todos.json file.
func (f *FileStore) GetLocation() string {
dir, _ := os.Getwd()
localrepo := fmt.Sprintf("%s/.todos.json", dir)
_, ferr := os.Stat(localrepo)
if ferr == nil {
return localrepo
if f.LocalTodosFileExists() {
return FILENAME
}

home := UserHomeDir()
return fmt.Sprintf("%s/.todos.json", home)
return fmt.Sprintf("%s/.todos.json", UserHomeDir())
}
4 changes: 4 additions & 0 deletions ultralist/memory_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ func (m *MemoryStore) Load() ([]*Todo, error) {
return m.Todos, nil
}

func (m *MemoryStore) LocalTodosFileExists() bool {
return false
}

// Save is saving todos to the memory store.
func (m *MemoryStore) Save(todos []*Todo) {
m.Todos = todos
Expand Down
1 change: 1 addition & 0 deletions ultralist/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ultralist
// Store is the interface for ultralist todos.
type Store interface {
GetLocation() string
LocalTodosFileExists() bool
Initialize()
Load() ([]*Todo, error)
Save(todos []*Todo)
Expand Down