177 lines
3.7 KiB
Go
177 lines
3.7 KiB
Go
// task/store.go – JSON-Persistenz für Tasks (atomisches Schreiben)
|
||
package task
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"sync"
|
||
"time"
|
||
|
||
"my-brain-importer/internal/config"
|
||
)
|
||
|
||
// Task repräsentiert eine Aufgabe.
|
||
type Task struct {
|
||
ID string `json:"id"`
|
||
Text string `json:"text"`
|
||
Done bool `json:"done"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
DoneAt *time.Time `json:"done_at,omitempty"`
|
||
DueDate *time.Time `json:"due_date,omitempty"`
|
||
Priority string `json:"priority,omitempty"` // "hoch", "mittel", "niedrig"
|
||
}
|
||
|
||
// Store verwaltet tasks.json mit atomischen Schreiboperationen.
|
||
type Store struct {
|
||
mu sync.Mutex
|
||
path string
|
||
}
|
||
|
||
// NewStore erstellt einen Store mit dem Pfad aus der Config.
|
||
func NewStore() *Store {
|
||
path := config.Cfg.Tasks.StorePath
|
||
if path == "" {
|
||
path = "./tasks.json"
|
||
}
|
||
return &Store{path: path}
|
||
}
|
||
|
||
// Load liest alle Tasks aus der JSON-Datei.
|
||
func (s *Store) Load() ([]Task, error) {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
data, err := os.ReadFile(s.path)
|
||
if os.IsNotExist(err) {
|
||
return []Task{}, nil
|
||
}
|
||
if err != nil {
|
||
return nil, fmt.Errorf("tasks lesen: %w", err)
|
||
}
|
||
|
||
var tasks []Task
|
||
if err := json.Unmarshal(data, &tasks); err != nil {
|
||
return nil, fmt.Errorf("tasks parsen: %w", err)
|
||
}
|
||
return tasks, nil
|
||
}
|
||
|
||
// save schreibt alle Tasks atomisch. Muss unter mu aufgerufen werden.
|
||
func (s *Store) save(tasks []Task) error {
|
||
data, err := json.MarshalIndent(tasks, "", " ")
|
||
if err != nil {
|
||
return fmt.Errorf("tasks serialisieren: %w", err)
|
||
}
|
||
tmp := s.path + ".tmp"
|
||
if err := os.WriteFile(tmp, data, 0644); err != nil {
|
||
return fmt.Errorf("temp-datei schreiben: %w", err)
|
||
}
|
||
return os.Rename(tmp, s.path)
|
||
}
|
||
|
||
// Add fügt einen neuen Task hinzu.
|
||
func (s *Store) Add(text, priority string, dueDate *time.Time) (Task, error) {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
tasks, err := s.loadLocked()
|
||
if err != nil {
|
||
return Task{}, err
|
||
}
|
||
|
||
t := Task{
|
||
ID: fmt.Sprintf("%d", time.Now().UnixNano()),
|
||
Text: text,
|
||
Done: false,
|
||
CreatedAt: time.Now(),
|
||
Priority: priority,
|
||
DueDate: dueDate,
|
||
}
|
||
tasks = append(tasks, t)
|
||
|
||
if err := s.save(tasks); err != nil {
|
||
return Task{}, err
|
||
}
|
||
return t, nil
|
||
}
|
||
|
||
// MarkDone markiert einen Task als erledigt.
|
||
func (s *Store) MarkDone(id string) error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
tasks, err := s.loadLocked()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
found := false
|
||
now := time.Now()
|
||
for i, t := range tasks {
|
||
if t.ID == id {
|
||
tasks[i].Done = true
|
||
tasks[i].DoneAt = &now
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
return fmt.Errorf("task %q nicht gefunden", id)
|
||
}
|
||
return s.save(tasks)
|
||
}
|
||
|
||
// Delete löscht einen Task.
|
||
func (s *Store) Delete(id string) error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
tasks, err := s.loadLocked()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
newTasks := tasks[:0]
|
||
for _, t := range tasks {
|
||
if t.ID != id {
|
||
newTasks = append(newTasks, t)
|
||
}
|
||
}
|
||
if len(newTasks) == len(tasks) {
|
||
return fmt.Errorf("task %q nicht gefunden", id)
|
||
}
|
||
return s.save(newTasks)
|
||
}
|
||
|
||
// OpenTasks gibt alle offenen Tasks zurück.
|
||
func (s *Store) OpenTasks() ([]Task, error) {
|
||
tasks, err := s.Load()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var open []Task
|
||
for _, t := range tasks {
|
||
if !t.Done {
|
||
open = append(open, t)
|
||
}
|
||
}
|
||
return open, nil
|
||
}
|
||
|
||
// loadLocked liest ohne eigenes Lock (muss unter s.mu aufgerufen werden).
|
||
func (s *Store) loadLocked() ([]Task, error) {
|
||
data, err := os.ReadFile(s.path)
|
||
if os.IsNotExist(err) {
|
||
return []Task{}, nil
|
||
}
|
||
if err != nil {
|
||
return nil, fmt.Errorf("tasks lesen: %w", err)
|
||
}
|
||
var tasks []Task
|
||
if err := json.Unmarshal(data, &tasks); err != nil {
|
||
return nil, fmt.Errorf("tasks parsen: %w", err)
|
||
}
|
||
return tasks, nil
|
||
}
|