Added update command
All checks were successful
build and deploy kleinTodo / build (push) Successful in 34s

Updated delete logic
Added last modified and deleted to the todo item
This commit is contained in:
Darius klein 2026-01-11 18:40:38 +01:00
parent bb685be5d5
commit 6e78c1948e
8 changed files with 133 additions and 21 deletions

View File

@ -2,10 +2,13 @@ package main
import ( import (
"context" "context"
"fmt"
"log" "log"
"log/slog" "log/slog"
"net/mail" "net/mail"
"os" "os"
"os/exec"
"runtime/debug"
"gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/clientCommon/config" "gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/clientCommon/config"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
@ -48,5 +51,44 @@ func commands() []*cli.Command {
Sync(), Sync(),
Add(), Add(),
Todo(), 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
}

View File

@ -28,7 +28,7 @@ func syncAction(context context.Context, c *cli.Command) error {
if err != nil { if err != nil {
return err return err
} }
serverTodos := store.GetTodoList(cfg.Server.Credentials.Username) serverTodos := store.GetTodoList(cfg.Server.Credentials.Username, true)
var todos []common.Todo var todos []common.Todo

View File

@ -4,10 +4,12 @@ import (
"bufio" "bufio"
"context" "context"
"fmt" "fmt"
"log"
"log/slog" "log/slog"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"time"
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common" "gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
@ -32,7 +34,7 @@ func todo(context context.Context, c *cli.Command) error {
return err return err
} }
todos := store.GetTodoList(cfg.Server.Credentials.Username) todos := store.GetTodoList(cfg.Server.Credentials.Username, false)
if err != nil { if err != nil {
return err return err
} }
@ -89,7 +91,9 @@ func handleDelete(scanner *bufio.Scanner, todos []common.Todo, store *common.Bol
removedItem := todos[index-1] 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 { if err != nil {
slog.Error(err.Error()) slog.Error(err.Error())
return todos return todos
@ -159,8 +163,16 @@ func handleAdd(scanner *bufio.Scanner, todos []common.Todo, store *common.BoltSt
scanner.Scan() scanner.Scan()
description := strings.TrimSpace(scanner.Text()) 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{ 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) err := newTodo.Store(store, cfg.Server.Credentials.Username)
if err != nil { if err != nil {

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath"
"sync" "sync"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
@ -24,8 +25,26 @@ type BoltStore struct {
DB *bolt.DB 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) { func NewBoltStore(path string) (*BoltStore, error) {
os.Mkdir("data", 0755) os.MkdirAll(filepath.Dir(path), 0755)
db, err := bolt.Open(path, 0600, nil) db, err := bolt.Open(path, 0600, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not open db: %w", err) return nil, fmt.Errorf("could not open db: %w", err)
@ -45,8 +64,10 @@ var (
func GetTodoDataStore() (*BoltStore, error) { func GetTodoDataStore() (*BoltStore, error) {
once.Do(func() { once.Do(func() {
var path string
path, err = getStoragePath()
// We assign to the outer 'dataStore' and 'err' variables. // We assign to the outer 'dataStore' and 'err' variables.
dataStore, err = NewBoltStore("data/todo.db") dataStore, err = NewBoltStore(path + "/todo.db")
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -193,15 +214,23 @@ func (s *BoltStore) GetTodoMap(user string) map[string]Todo {
return serverTodos return serverTodos
} }
func (s *BoltStore) GetTodoList(user string) []Todo { func (s *BoltStore) GetTodoList(user string, includeDeleted bool) []Todo {
storedTodoJsons := s.GetAllFromBucket(user) storedTodoJsons := s.GetAllFromBucket(user)
var serverTodos []Todo var storedTodos []Todo
for _, val := range storedTodoJsons { for _, val := range storedTodoJsons {
var todo Todo var todo Todo
if json.Unmarshal([]byte(val), &todo) == nil { 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
} }

View File

@ -4,12 +4,14 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings" "strings"
"time"
) )
func (todo Todo) Store(store *BoltStore, user string) error { func (todo Todo) Store(store *BoltStore, user string) error {
if todo.Owner != user { if todo.Owner != user {
return fmt.Errorf("unauthorized user") return fmt.Errorf("unauthorized user")
} }
todo.LastModified = time.Now()
todoJson, err := json.Marshal(todo) todoJson, err := json.Marshal(todo)
if err != nil { if err != nil {
return err return err
@ -19,10 +21,11 @@ func (todo Todo) Store(store *BoltStore, user string) error {
func (todoRequest StoreTodoRequest) Store(store *BoltStore, user string) error { func (todoRequest StoreTodoRequest) Store(store *BoltStore, user string) error {
todo := Todo{ todo := Todo{
Name: todoRequest.Name, Name: todoRequest.Name,
Description: todoRequest.Description, Description: todoRequest.Description,
Status: todoRequest.Status, Status: todoRequest.Status,
Owner: user, Owner: user,
LastModified: time.Now(),
} }
todoJson, err := json.Marshal(todo) todoJson, err := json.Marshal(todo)
if err != nil { if err != nil {
@ -41,6 +44,10 @@ func (todoList TodoList) FindByName(name string) (Todo, bool) {
} }
func (todo Todo) PrintIndexed(index int) { func (todo Todo) PrintIndexed(index int) {
if todo.Deleted {
return
}
var statusColor string var statusColor string
// Select color based on the status (case-insensitive) // 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 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("%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%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
} }

View File

@ -1,15 +1,19 @@
package common package common
import "time"
type Credentials struct { type Credentials struct {
Username string `json:"username" toml:"username"` Username string `json:"username" toml:"username"`
Password string `json:"password" toml:"password"` Password string `json:"password" toml:"password"`
} }
type Todo struct { type Todo struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Status string `json:"status"` Status string `json:"status"`
Owner string `json:"owner"` Owner string `json:"owner"`
LastModified time.Time `json:"last_modified"`
Deleted bool `json:"deleted"`
} }
type StoreTodoRequest struct { type StoreTodoRequest struct {

View File

@ -18,5 +18,7 @@ FROM gcr.io/distroless/base-debian12
COPY --from=build /app/serverBinary . COPY --from=build /app/serverBinary .
ENV XDG_CONFIG_HOME=/data
# Define the command to run the app when the container starts # Define the command to run the app when the container starts
CMD ["./serverBinary"] CMD ["./serverBinary"]

View File

@ -3,7 +3,6 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"reflect"
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common" "gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common/jwt" "gitea.kleinsense.nl/DariusKlein/kleinTodo/common/jwt"
@ -36,14 +35,19 @@ func SyncHandler(w http.ResponseWriter, r *http.Request) {
for _, clientTodo := range todoList.Todos { for _, clientTodo := range todoList.Todos {
serverTodo, exists := serverTodos[clientTodo.Name] 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) err = clientTodo.Store(store, user)
if handleError(w, http.StatusInternalServerError, err) { if handleError(w, http.StatusInternalServerError, err) {
return return
} }
serverTodos[clientTodo.Name] = clientTodo serverTodos[clientTodo.Name] = clientTodo
} else { } else {
if !reflect.DeepEqual(serverTodo, clientTodo) { if !serverTodo.IsEqual(clientTodo) {
response.MisMatchingTodos = append(response.MisMatchingTodos, common.MisMatchingTodo{ response.MisMatchingTodos = append(response.MisMatchingTodos, common.MisMatchingTodo{
ServerTodo: serverTodo, ServerTodo: serverTodo,
LocalTodo: clientTodo, LocalTodo: clientTodo,