From 6e78c1948efba92abece106738dabe68f5952f5d Mon Sep 17 00:00:00 2001 From: Darius klein Date: Sun, 11 Jan 2026 18:40:38 +0100 Subject: [PATCH] Added update command Updated delete logic Added last modified and deleted to the todo item --- client/todo/main.go | 42 +++++++++++++++++++++++++++++++++++ client/todo/sync.go | 2 +- client/todo/todo.go | 18 ++++++++++++--- common/bolt.go | 41 +++++++++++++++++++++++++++++----- common/todo.go | 27 ++++++++++++++++++---- common/types.go | 12 ++++++---- server/Dockerfile | 2 ++ server/handler/syncHandler.go | 10 ++++++--- 8 files changed, 133 insertions(+), 21 deletions(-) diff --git a/client/todo/main.go b/client/todo/main.go index b428cfc..92b5df9 100644 --- a/client/todo/main.go +++ b/client/todo/main.go @@ -2,10 +2,13 @@ package main import ( "context" + "fmt" "log" "log/slog" "net/mail" "os" + "os/exec" + "runtime/debug" "gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/clientCommon/config" "github.com/urfave/cli/v3" @@ -48,5 +51,44 @@ func commands() []*cli.Command { Sync(), Add(), Todo(), + { + Name: "update", + Usage: "Update the vennexCLI to a specific version", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "version", + Usage: "The version label to install (e.g., v1.2.0, main, latest)", + Value: "latest", + }, + }, + Action: runUpdate, + }, } } + +func runUpdate(ctx context.Context, command *cli.Command) error { + var pkgPath = "unknown" + info, ok := debug.ReadBuildInfo() + if ok { + pkgPath = info.Main.Path + } + + pkg := fmt.Sprintf("%s@%s", pkgPath, command.String("version")) + + slog.Info(fmt.Sprintf("Updating to %s...\n", pkg)) + + cmd := exec.Command("go", "install", pkg) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + slog.Error("Update failed:", "error", err) + return err + } + + slog.Info("Update successful!") + + return nil +} diff --git a/client/todo/sync.go b/client/todo/sync.go index 473654a..5ff915a 100644 --- a/client/todo/sync.go +++ b/client/todo/sync.go @@ -28,7 +28,7 @@ func syncAction(context context.Context, c *cli.Command) error { if err != nil { return err } - serverTodos := store.GetTodoList(cfg.Server.Credentials.Username) + serverTodos := store.GetTodoList(cfg.Server.Credentials.Username, true) var todos []common.Todo diff --git a/client/todo/todo.go b/client/todo/todo.go index bb4441d..bd8ce2f 100644 --- a/client/todo/todo.go +++ b/client/todo/todo.go @@ -4,10 +4,12 @@ import ( "bufio" "context" "fmt" + "log" "log/slog" "os" "strconv" "strings" + "time" "gitea.kleinsense.nl/DariusKlein/kleinTodo/common" "github.com/urfave/cli/v3" @@ -32,7 +34,7 @@ func todo(context context.Context, c *cli.Command) error { return err } - todos := store.GetTodoList(cfg.Server.Credentials.Username) + todos := store.GetTodoList(cfg.Server.Credentials.Username, false) if err != nil { return err } @@ -89,7 +91,9 @@ func handleDelete(scanner *bufio.Scanner, todos []common.Todo, store *common.Bol removedItem := todos[index-1] - err = store.RemoveValueFromBucket(cfg.Server.Credentials.Username, removedItem.Name) + removedItem.Deleted = true + + err = removedItem.Store(store, cfg.Server.Credentials.Username) if err != nil { slog.Error(err.Error()) return todos @@ -159,8 +163,16 @@ func handleAdd(scanner *bufio.Scanner, todos []common.Todo, store *common.BoltSt scanner.Scan() description := strings.TrimSpace(scanner.Text()) + todoList := common.TodoList{Todos: todos} + + _, exists := todoList.FindByName(name) + if exists { + log.Fatalf("Item '%s' already exists.", name) + return todos + } + newTodo := common.Todo{ - Name: name, Description: description, Status: common.NotStarted, Owner: cfg.Server.Credentials.Username, + Name: name, Description: description, Status: common.NotStarted, Owner: cfg.Server.Credentials.Username, LastModified: time.Now(), } err := newTodo.Store(store, cfg.Server.Credentials.Username) if err != nil { diff --git a/common/bolt.go b/common/bolt.go index 7e08d7a..51c2da2 100644 --- a/common/bolt.go +++ b/common/bolt.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "sync" bolt "go.etcd.io/bbolt" @@ -24,8 +25,26 @@ type BoltStore struct { DB *bolt.DB } +const configDirName = "kleinTodo" + +func getStoragePath() (path string, err error) { + configDir, err := os.UserConfigDir() + if err != nil { + return "", fmt.Errorf("could not get user config directory: %w", err) + } + + appConfigDir := filepath.Join(configDir, configDirName) + + // Ensure the directory exists before we try to use it. + if err := os.MkdirAll(appConfigDir, 0755); err != nil { + return "", fmt.Errorf("could not create app config directory: %w", err) + } + + return appConfigDir, nil +} + func NewBoltStore(path string) (*BoltStore, error) { - os.Mkdir("data", 0755) + os.MkdirAll(filepath.Dir(path), 0755) db, err := bolt.Open(path, 0600, nil) if err != nil { return nil, fmt.Errorf("could not open db: %w", err) @@ -45,8 +64,10 @@ var ( func GetTodoDataStore() (*BoltStore, error) { once.Do(func() { + var path string + path, err = getStoragePath() // We assign to the outer 'dataStore' and 'err' variables. - dataStore, err = NewBoltStore("data/todo.db") + dataStore, err = NewBoltStore(path + "/todo.db") }) if err != nil { return nil, err @@ -193,15 +214,23 @@ func (s *BoltStore) GetTodoMap(user string) map[string]Todo { return serverTodos } -func (s *BoltStore) GetTodoList(user string) []Todo { +func (s *BoltStore) GetTodoList(user string, includeDeleted bool) []Todo { storedTodoJsons := s.GetAllFromBucket(user) - var serverTodos []Todo + var storedTodos []Todo for _, val := range storedTodoJsons { var todo Todo if json.Unmarshal([]byte(val), &todo) == nil { - serverTodos = append(serverTodos, todo) + var include = false + if includeDeleted { + include = true + } else { + include = todo.Deleted + } + if include { + storedTodos = append(storedTodos, todo) + } } } - return serverTodos + return storedTodos } diff --git a/common/todo.go b/common/todo.go index 9ac4479..1185d09 100644 --- a/common/todo.go +++ b/common/todo.go @@ -4,12 +4,14 @@ import ( "encoding/json" "fmt" "strings" + "time" ) func (todo Todo) Store(store *BoltStore, user string) error { if todo.Owner != user { return fmt.Errorf("unauthorized user") } + todo.LastModified = time.Now() todoJson, err := json.Marshal(todo) if err != nil { return err @@ -19,10 +21,11 @@ func (todo Todo) Store(store *BoltStore, user string) error { func (todoRequest StoreTodoRequest) Store(store *BoltStore, user string) error { todo := Todo{ - Name: todoRequest.Name, - Description: todoRequest.Description, - Status: todoRequest.Status, - Owner: user, + Name: todoRequest.Name, + Description: todoRequest.Description, + Status: todoRequest.Status, + Owner: user, + LastModified: time.Now(), } todoJson, err := json.Marshal(todo) if err != nil { @@ -41,6 +44,10 @@ func (todoList TodoList) FindByName(name string) (Todo, bool) { } func (todo Todo) PrintIndexed(index int) { + + if todo.Deleted { + return + } var statusColor string // Select color based on the status (case-insensitive) @@ -57,7 +64,19 @@ func (todo Todo) PrintIndexed(index int) { statusColor = ColorReset // No color for unknown statuses } + lastMod := todo.LastModified.Format("2006-01-02 15:04") + fmt.Printf("%d) %s - %s%s%s\n", index, todo.Name, statusColor, strings.ToUpper(todo.Status), ColorReset) fmt.Printf("\t%s\n", todo.Description) + + fmt.Printf("\t%sLast Modified: %s%s\n", ColorReset, lastMod, ColorReset) +} + +func (t Todo) IsEqual(other Todo) bool { + return t.Name == other.Name && + t.Description == other.Description && + t.Status == other.Status && + t.Owner == other.Owner && + t.Deleted == other.Deleted } diff --git a/common/types.go b/common/types.go index 182006d..9b496f7 100644 --- a/common/types.go +++ b/common/types.go @@ -1,15 +1,19 @@ package common +import "time" + type Credentials struct { Username string `json:"username" toml:"username"` Password string `json:"password" toml:"password"` } type Todo struct { - Name string `json:"name"` - Description string `json:"description"` - Status string `json:"status"` - Owner string `json:"owner"` + Name string `json:"name"` + Description string `json:"description"` + Status string `json:"status"` + Owner string `json:"owner"` + LastModified time.Time `json:"last_modified"` + Deleted bool `json:"deleted"` } type StoreTodoRequest struct { diff --git a/server/Dockerfile b/server/Dockerfile index 04b5b7f..46686bc 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -18,5 +18,7 @@ FROM gcr.io/distroless/base-debian12 COPY --from=build /app/serverBinary . +ENV XDG_CONFIG_HOME=/data + # Define the command to run the app when the container starts CMD ["./serverBinary"] diff --git a/server/handler/syncHandler.go b/server/handler/syncHandler.go index 14ea561..5d3730e 100644 --- a/server/handler/syncHandler.go +++ b/server/handler/syncHandler.go @@ -3,7 +3,6 @@ package handler import ( "encoding/json" "net/http" - "reflect" "gitea.kleinsense.nl/DariusKlein/kleinTodo/common" "gitea.kleinsense.nl/DariusKlein/kleinTodo/common/jwt" @@ -36,14 +35,19 @@ func SyncHandler(w http.ResponseWriter, r *http.Request) { for _, clientTodo := range todoList.Todos { serverTodo, exists := serverTodos[clientTodo.Name] - if !exists { + if clientTodo.Deleted && clientTodo.LastModified.After(serverTodo.LastModified) { + err = store.RemoveValueFromBucket(user, clientTodo.Name) + if handleError(w, http.StatusInternalServerError, err) { + return + } + } else if !exists { err = clientTodo.Store(store, user) if handleError(w, http.StatusInternalServerError, err) { return } serverTodos[clientTodo.Name] = clientTodo } else { - if !reflect.DeepEqual(serverTodo, clientTodo) { + if !serverTodo.IsEqual(clientTodo) { response.MisMatchingTodos = append(response.MisMatchingTodos, common.MisMatchingTodo{ ServerTodo: serverTodo, LocalTodo: clientTodo,