local client with sync and today
This commit is contained in:
parent
4b6167cbd0
commit
d82f4b7c0a
|
@ -2,4 +2,5 @@
|
|||
/gte-generate-recurring
|
||||
/gte
|
||||
/gte-daemon
|
||||
test.db
|
||||
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.ewintr.nl/gte/internal/configuration"
|
||||
"git.ewintr.nl/gte/internal/process"
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
"git.ewintr.nl/gte/pkg/mstore"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInitCommand = errors.New("could not initialize command")
|
||||
ErrFailedCommand = errors.New("could not execute command")
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
type Command interface {
|
||||
Do() (Result, error)
|
||||
}
|
||||
|
||||
func Parse(args []string, conf *configuration.Configuration) (Command, error) {
|
||||
if len(args) == 0 {
|
||||
return NewEmpty()
|
||||
}
|
||||
|
||||
cmd, _ := args[0], args[1:]
|
||||
switch cmd {
|
||||
case "sync":
|
||||
return NewSync(conf)
|
||||
case "today":
|
||||
return NewToday(conf)
|
||||
default:
|
||||
return NewEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
type Empty struct{}
|
||||
|
||||
func NewEmpty() (*Empty, error) {
|
||||
return &Empty{}, nil
|
||||
}
|
||||
|
||||
func (cmd *Empty) Do() (Result, error) {
|
||||
return Result{
|
||||
Message: "did nothing\n",
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Sync struct {
|
||||
syncer *process.Sync
|
||||
}
|
||||
|
||||
func NewSync(conf *configuration.Configuration) (*Sync, error) {
|
||||
msgStore := mstore.NewIMAP(conf.IMAP())
|
||||
remote := storage.NewRemoteRepository(msgStore)
|
||||
local, err := storage.NewSqlite(conf.Sqlite())
|
||||
if err != nil {
|
||||
return &Sync{}, fmt.Errorf("%w: %v", ErrInitCommand, err)
|
||||
}
|
||||
syncer := process.NewSync(remote, local)
|
||||
|
||||
return &Sync{
|
||||
syncer: syncer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Sync) Do() (Result, error) {
|
||||
result, err := s.syncer.Process()
|
||||
if err != nil {
|
||||
return Result{}, fmt.Errorf("%w: %v", ErrFailedCommand, err)
|
||||
}
|
||||
|
||||
return Result{
|
||||
Message: fmt.Sprintf("synced %d tasks\n", result.Count),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Today struct {
|
||||
local storage.LocalRepository
|
||||
}
|
||||
|
||||
func NewToday(conf *configuration.Configuration) (*Today, error) {
|
||||
local, err := storage.NewSqlite(conf.Sqlite())
|
||||
if err != nil {
|
||||
return &Today{}, fmt.Errorf("%w: %v", ErrInitCommand, err)
|
||||
}
|
||||
|
||||
return &Today{
|
||||
local: local,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Today) Do() (Result, error) {
|
||||
tasks, err := t.local.FindAllInFolder(task.FOLDER_PLANNED)
|
||||
if err != nil {
|
||||
return Result{}, fmt.Errorf("%w: %v", ErrFailedCommand, err)
|
||||
}
|
||||
|
||||
todayTasks := []*task.Task{}
|
||||
for _, t := range tasks {
|
||||
if t.Due == task.Today || task.Today.After(t.Due) {
|
||||
todayTasks = append(todayTasks, t)
|
||||
}
|
||||
}
|
||||
|
||||
if len(todayTasks) == 0 {
|
||||
return Result{
|
||||
Message: "nothing left",
|
||||
}, nil
|
||||
}
|
||||
|
||||
var msg string
|
||||
for _, t := range todayTasks {
|
||||
msg += fmt.Sprintf("%s - %s\n", t.Project, t.Action)
|
||||
}
|
||||
|
||||
return Result{
|
||||
Message: msg,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.ewintr.nl/go-kit/log"
|
||||
"git.ewintr.nl/gte/cmd/cli/command"
|
||||
"git.ewintr.nl/gte/internal/configuration"
|
||||
)
|
||||
|
||||
func main() {
|
||||
loglevel := log.LogLevel("error")
|
||||
if os.Getenv("GTE_LOGLEVEL") != "" {
|
||||
loglevel = log.LogLevel(os.Getenv("GTE_LOGLEVEL"))
|
||||
}
|
||||
logger := log.New(os.Stdout).WithField("cmd", "cli")
|
||||
logger.SetLogLevel(loglevel)
|
||||
|
||||
configPath := "/home/erik/.config/gte/gte.conf"
|
||||
if os.Getenv("GTE_CONFIG") != "" {
|
||||
configPath = os.Getenv("GTE_CONFIG")
|
||||
}
|
||||
configFile, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
logger.WithErr(err).Error("could not open config file")
|
||||
os.Exit(1)
|
||||
}
|
||||
config := configuration.New(configFile)
|
||||
|
||||
cmd, err := command.Parse(os.Args[1:], config)
|
||||
if err != nil {
|
||||
logger.WithErr(err).Error("could not initialize command")
|
||||
os.Exit(1)
|
||||
}
|
||||
result, err := cmd.Do()
|
||||
if err != nil {
|
||||
logger.WithErr(err).Error("could perform command")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("%s\n", result.Message)
|
||||
}
|
|
@ -9,6 +9,7 @@ import (
|
|||
"git.ewintr.nl/go-kit/log"
|
||||
"git.ewintr.nl/gte/internal/configuration"
|
||||
"git.ewintr.nl/gte/internal/process"
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
"git.ewintr.nl/gte/pkg/msend"
|
||||
"git.ewintr.nl/gte/pkg/mstore"
|
||||
|
@ -35,8 +36,8 @@ func main() {
|
|||
|
||||
msgStore := mstore.NewIMAP(config.IMAP())
|
||||
mailSend := msend.NewSSLSMTP(config.SMTP())
|
||||
repo := task.NewRemoteRepository(msgStore)
|
||||
disp := task.NewDispatcher(mailSend)
|
||||
repo := storage.NewRemoteRepository(msgStore)
|
||||
disp := storage.NewDispatcher(mailSend)
|
||||
|
||||
inboxProc := process.NewInbox(repo)
|
||||
recurProc := process.NewRecur(repo, disp, *daysAhead)
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"git.ewintr.nl/go-kit/log"
|
||||
"git.ewintr.nl/gte/internal/configuration"
|
||||
"git.ewintr.nl/gte/internal/process"
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/pkg/msend"
|
||||
"git.ewintr.nl/gte/pkg/mstore"
|
||||
)
|
||||
|
@ -28,8 +28,8 @@ func main() {
|
|||
|
||||
msgStore := mstore.NewIMAP(config.IMAP())
|
||||
mailSend := msend.NewSSLSMTP(config.SMTP())
|
||||
taskRepo := task.NewRemoteRepository(msgStore)
|
||||
taskDisp := task.NewDispatcher(mailSend)
|
||||
taskRepo := storage.NewRemoteRepository(msgStore)
|
||||
taskDisp := storage.NewDispatcher(mailSend)
|
||||
recur := process.NewRecur(taskRepo, taskDisp, *daysAhead)
|
||||
|
||||
result, err := recur.Process()
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"git.ewintr.nl/go-kit/log"
|
||||
"git.ewintr.nl/gte/internal/configuration"
|
||||
"git.ewintr.nl/gte/internal/process"
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/pkg/mstore"
|
||||
)
|
||||
|
||||
|
@ -24,7 +24,7 @@ func main() {
|
|||
}
|
||||
config := configuration.New(configFile)
|
||||
msgStore := mstore.NewIMAP(config.IMAP())
|
||||
inboxProcessor := process.NewInbox(task.NewRemoteRepository(msgStore))
|
||||
inboxProcessor := process.NewInbox(storage.NewRemoteRepository(msgStore))
|
||||
|
||||
result, err := inboxProcessor.Process()
|
||||
if err != nil {
|
||||
|
|
2
go.mod
2
go.mod
|
@ -7,5 +7,5 @@ require (
|
|||
github.com/emersion/go-imap v1.1.0
|
||||
github.com/emersion/go-message v0.14.1
|
||||
github.com/google/uuid v1.2.0
|
||||
modernc.org/sqlite v1.11.1 // indirect
|
||||
modernc.org/sqlite v1.11.1
|
||||
)
|
||||
|
|
|
@ -5,7 +5,9 @@ import (
|
|||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/pkg/msend"
|
||||
"git.ewintr.nl/gte/pkg/mstore"
|
||||
)
|
||||
|
@ -28,6 +30,12 @@ type Configuration struct {
|
|||
|
||||
ToName string
|
||||
ToAddress string
|
||||
|
||||
LocalDBPath string
|
||||
}
|
||||
|
||||
type LocalConfiguration struct {
|
||||
MinSyncInterval time.Duration
|
||||
}
|
||||
|
||||
func New(src io.Reader) *Configuration {
|
||||
|
@ -62,6 +70,8 @@ func New(src io.Reader) *Configuration {
|
|||
conf.FromName = value
|
||||
case "from_address":
|
||||
conf.FromAddress = value
|
||||
case "local_db_path":
|
||||
conf.LocalDBPath = value
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,3 +95,9 @@ func (c *Configuration) SMTP() *msend.SSLSMTPConfig {
|
|||
To: c.ToAddress,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Configuration) Sqlite() *storage.SqliteConfig {
|
||||
return &storage.SqliteConfig{
|
||||
DBPath: c.LocalDBPath,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"git.ewintr.nl/go-kit/test"
|
||||
"git.ewintr.nl/gte/internal/configuration"
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/pkg/msend"
|
||||
"git.ewintr.nl/gte/pkg/mstore"
|
||||
)
|
||||
|
@ -67,6 +68,13 @@ func TestNew(t *testing.T) {
|
|||
FromAddress: "from_address",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "local",
|
||||
source: "local_db_path=path",
|
||||
exp: &configuration.Configuration{
|
||||
LocalDBPath: "path",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
test.Equals(t, tc.exp, configuration.New(strings.NewReader(tc.source)))
|
||||
|
@ -86,6 +94,7 @@ func TestConfigs(t *testing.T) {
|
|||
ToAddress: "to_address",
|
||||
FromName: "from_name",
|
||||
FromAddress: "from_address",
|
||||
LocalDBPath: "db_path",
|
||||
}
|
||||
|
||||
t.Run("imap", func(t *testing.T) {
|
||||
|
@ -108,4 +117,11 @@ func TestConfigs(t *testing.T) {
|
|||
}
|
||||
test.Equals(t, exp, conf.SMTP())
|
||||
})
|
||||
|
||||
t.Run("sqlite", func(t *testing.T) {
|
||||
exp := &storage.SqliteConfig{
|
||||
DBPath: "db_path",
|
||||
}
|
||||
test.Equals(t, exp, conf.Sqlite())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
)
|
||||
|
||||
|
@ -15,8 +16,9 @@ var (
|
|||
inboxLock sync.Mutex
|
||||
)
|
||||
|
||||
// Inbox processes all messages in INBOX in a remote repository
|
||||
type Inbox struct {
|
||||
taskRepo *task.RemoteRepository
|
||||
taskRepo *storage.RemoteRepository
|
||||
}
|
||||
|
||||
type InboxResult struct {
|
||||
|
@ -24,7 +26,7 @@ type InboxResult struct {
|
|||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func NewInbox(repo *task.RemoteRepository) *Inbox {
|
||||
func NewInbox(repo *storage.RemoteRepository) *Inbox {
|
||||
return &Inbox{
|
||||
taskRepo: repo,
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"git.ewintr.nl/go-kit/test"
|
||||
"git.ewintr.nl/gte/internal/process"
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
"git.ewintr.nl/gte/pkg/mstore"
|
||||
)
|
||||
|
@ -107,7 +108,7 @@ func TestInboxProcess(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
inboxProc := process.NewInbox(task.NewRemoteRepository(mstorer))
|
||||
inboxProc := process.NewInbox(storage.NewRemoteRepository(mstorer))
|
||||
actResult, err := inboxProc.Process()
|
||||
|
||||
test.OK(t, err)
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
)
|
||||
|
||||
|
@ -15,9 +16,10 @@ var (
|
|||
recurLock sync.Mutex
|
||||
)
|
||||
|
||||
// Recur generates new tasks from a recurring task for a given day
|
||||
type Recur struct {
|
||||
taskRepo *task.RemoteRepository
|
||||
taskDispatcher *task.Dispatcher
|
||||
taskRepo *storage.RemoteRepository
|
||||
taskDispatcher *storage.Dispatcher
|
||||
daysAhead int
|
||||
}
|
||||
|
||||
|
@ -26,7 +28,7 @@ type RecurResult struct {
|
|||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func NewRecur(repo *task.RemoteRepository, disp *task.Dispatcher, daysAhead int) *Recur {
|
||||
func NewRecur(repo *storage.RemoteRepository, disp *storage.Dispatcher, daysAhead int) *Recur {
|
||||
return &Recur{
|
||||
taskRepo: repo,
|
||||
taskDispatcher: disp,
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
|
||||
"git.ewintr.nl/go-kit/test"
|
||||
"git.ewintr.nl/gte/internal/process"
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
"git.ewintr.nl/gte/pkg/msend"
|
||||
"git.ewintr.nl/gte/pkg/mstore"
|
||||
|
@ -54,7 +55,7 @@ func TestRecurProcess(t *testing.T) {
|
|||
}
|
||||
msender := msend.NewMemory()
|
||||
|
||||
recurProc := process.NewRecur(task.NewRemoteRepository(mstorer), task.NewDispatcher(msender), 1)
|
||||
recurProc := process.NewRecur(storage.NewRemoteRepository(mstorer), storage.NewDispatcher(msender), 1)
|
||||
actResult, err := recurProc.Process()
|
||||
test.OK(t, err)
|
||||
test.Equals(t, tc.expCount, actResult.Count)
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package process
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSyncProcess = errors.New("could not sync local repository")
|
||||
)
|
||||
|
||||
// Sync fetches all tasks in regular folders from the remote repository and overwrites what is stored locally
|
||||
type Sync struct {
|
||||
remote *storage.RemoteRepository
|
||||
local storage.LocalRepository
|
||||
}
|
||||
|
||||
type SyncResult struct {
|
||||
Duration string `json:"duration"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func NewSync(remote *storage.RemoteRepository, local storage.LocalRepository) *Sync {
|
||||
return &Sync{
|
||||
remote: remote,
|
||||
local: local,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sync) Process() (*SyncResult, error) {
|
||||
start := time.Now()
|
||||
|
||||
tasks := []*task.Task{}
|
||||
for _, folder := range task.KnownFolders {
|
||||
if folder == task.FOLDER_INBOX {
|
||||
continue
|
||||
}
|
||||
folderTasks, err := s.remote.FindAll(folder)
|
||||
if err != nil {
|
||||
return &SyncResult{}, fmt.Errorf("%w: %v", ErrSyncProcess, err)
|
||||
}
|
||||
|
||||
for _, t := range folderTasks {
|
||||
tasks = append(tasks, t)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.local.SetTasks(tasks); err != nil {
|
||||
return &SyncResult{}, fmt.Errorf("%w: %v", ErrSyncProcess, err)
|
||||
}
|
||||
|
||||
return &SyncResult{
|
||||
Duration: time.Since(start).String(),
|
||||
Count: len(tasks),
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package process_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.ewintr.nl/go-kit/test"
|
||||
"git.ewintr.nl/gte/internal/process"
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
"git.ewintr.nl/gte/pkg/mstore"
|
||||
)
|
||||
|
||||
func TestSyncProcess(t *testing.T) {
|
||||
task1 := &task.Task{
|
||||
Id: "id1",
|
||||
Version: 1,
|
||||
Action: "action1",
|
||||
Folder: task.FOLDER_NEW,
|
||||
}
|
||||
task2 := &task.Task{
|
||||
Id: "id2",
|
||||
Version: 1,
|
||||
Action: "action2",
|
||||
Folder: task.FOLDER_UNPLANNED,
|
||||
}
|
||||
|
||||
mstorer, err := mstore.NewMemory(task.KnownFolders)
|
||||
test.OK(t, err)
|
||||
test.OK(t, mstorer.Add(task1.Folder, task1.FormatSubject(), task1.FormatBody()))
|
||||
test.OK(t, mstorer.Add(task2.Folder, task2.FormatSubject(), task2.FormatBody()))
|
||||
remote := storage.NewRemoteRepository(mstorer)
|
||||
local := storage.NewMemory()
|
||||
|
||||
syncer := process.NewSync(remote, local)
|
||||
actResult, err := syncer.Process()
|
||||
test.OK(t, err)
|
||||
test.Equals(t, 2, actResult.Count)
|
||||
actTasks1, err := local.FindAllInFolder(task.FOLDER_NEW)
|
||||
test.OK(t, err)
|
||||
test.Equals(t, []*task.Task{task1}, actTasks1)
|
||||
actTasks2, err := local.FindAllInFolder(task.FOLDER_UNPLANNED)
|
||||
test.OK(t, err)
|
||||
test.Equals(t, []*task.Task{task2}, actTasks2)
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
package task
|
||||
package storage
|
||||
|
||||
import "git.ewintr.nl/gte/pkg/msend"
|
||||
import (
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
"git.ewintr.nl/gte/pkg/msend"
|
||||
)
|
||||
|
||||
type Dispatcher struct {
|
||||
msender msend.MSender
|
||||
|
@ -12,7 +15,7 @@ func NewDispatcher(msender msend.MSender) *Dispatcher {
|
|||
}
|
||||
}
|
||||
|
||||
func (d *Dispatcher) Dispatch(t *Task) error {
|
||||
func (d *Dispatcher) Dispatch(t *task.Task) error {
|
||||
return d.msender.Send(&msend.Message{
|
||||
Subject: t.FormatSubject(),
|
||||
Body: t.FormatBody(),
|
|
@ -1,17 +1,18 @@
|
|||
package task_test
|
||||
package storage_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.ewintr.nl/go-kit/test"
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
"git.ewintr.nl/gte/pkg/msend"
|
||||
)
|
||||
|
||||
func TestDispatcherDispatch(t *testing.T) {
|
||||
mem := msend.NewMemory()
|
||||
disp := task.NewDispatcher(mem)
|
||||
disp := storage.NewDispatcher(mem)
|
||||
tsk := &task.Task{
|
||||
Id: "id",
|
||||
Version: 3,
|
|
@ -0,0 +1,14 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
)
|
||||
|
||||
type LocalRepository interface {
|
||||
LatestSync() (time.Time, error)
|
||||
SetTasks(tasks []*task.Task) error
|
||||
FindAllInFolder(folder string) ([]*task.Task, error)
|
||||
FindAllInProject(project string) ([]*task.Task, error)
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
)
|
||||
|
||||
// Memory is an in memory implementation of LocalRepository
|
||||
type Memory struct {
|
||||
tasks []*task.Task
|
||||
latestSync time.Time
|
||||
}
|
||||
|
||||
func NewMemory() *Memory {
|
||||
return &Memory{
|
||||
tasks: []*task.Task{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Memory) LatestSync() (time.Time, error) {
|
||||
return m.latestSync, nil
|
||||
}
|
||||
|
||||
func (m *Memory) SetTasks(tasks []*task.Task) error {
|
||||
nTasks := []*task.Task{}
|
||||
for _, t := range tasks {
|
||||
nt := *t
|
||||
nt.Message = nil
|
||||
nTasks = append(nTasks, &nt)
|
||||
}
|
||||
m.tasks = nTasks
|
||||
m.latestSync = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Memory) FindAllInFolder(folder string) ([]*task.Task, error) {
|
||||
tasks := []*task.Task{}
|
||||
for _, t := range m.tasks {
|
||||
if t.Folder == folder {
|
||||
tasks = append(tasks, t)
|
||||
}
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func (m *Memory) FindAllInProject(project string) ([]*task.Task, error) {
|
||||
tasks := []*task.Task{}
|
||||
for _, t := range m.tasks {
|
||||
if t.Project == project {
|
||||
tasks = append(tasks, t)
|
||||
}
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package storage_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.ewintr.nl/go-kit/test"
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
"git.ewintr.nl/gte/pkg/mstore"
|
||||
)
|
||||
|
||||
func TestMemory(t *testing.T) {
|
||||
folder1, folder2 := "folder1", "folder2"
|
||||
project1, project2 := "project1", "project2"
|
||||
task1 := &task.Task{
|
||||
Folder: folder1,
|
||||
Project: project1,
|
||||
Action: "action1",
|
||||
Message: &mstore.Message{
|
||||
Subject: "action1",
|
||||
},
|
||||
}
|
||||
task2 := &task.Task{
|
||||
Folder: folder1,
|
||||
Project: project2,
|
||||
Action: "action2",
|
||||
Message: &mstore.Message{
|
||||
Subject: "action2",
|
||||
},
|
||||
}
|
||||
task3 := &task.Task{
|
||||
Folder: folder2,
|
||||
Project: project1,
|
||||
Action: "action3",
|
||||
Message: &mstore.Message{
|
||||
Subject: "action3",
|
||||
},
|
||||
}
|
||||
tasks := []*task.Task{task1, task2, task3}
|
||||
|
||||
t.Run("sync", func(t *testing.T) {
|
||||
mem := storage.NewMemory()
|
||||
latest, err := mem.LatestSync()
|
||||
test.OK(t, err)
|
||||
test.Assert(t, latest.IsZero(), "lastest was not zero")
|
||||
|
||||
start := time.Now()
|
||||
test.OK(t, mem.SetTasks(tasks))
|
||||
latest, err = mem.LatestSync()
|
||||
test.OK(t, err)
|
||||
test.Assert(t, latest.After(start), "latest was not after start")
|
||||
})
|
||||
|
||||
t.Run("findallinfolder", func(t *testing.T) {
|
||||
mem := storage.NewMemory()
|
||||
test.OK(t, mem.SetTasks(tasks))
|
||||
act, err := mem.FindAllInFolder(folder1)
|
||||
test.OK(t, err)
|
||||
exp := []*task.Task{task1, task2}
|
||||
for _, tsk := range exp {
|
||||
tsk.Message = nil
|
||||
}
|
||||
test.Equals(t, exp, act)
|
||||
})
|
||||
|
||||
t.Run("findallinproject", func(t *testing.T) {
|
||||
mem := storage.NewMemory()
|
||||
test.OK(t, mem.SetTasks(tasks))
|
||||
act, err := mem.FindAllInProject(project1)
|
||||
test.OK(t, err)
|
||||
exp := []*task.Task{task1, task3}
|
||||
for _, tsk := range exp {
|
||||
tsk.Message = nil
|
||||
}
|
||||
test.Equals(t, exp, act)
|
||||
})
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
package task
|
||||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
"git.ewintr.nl/gte/pkg/mstore"
|
||||
)
|
||||
|
||||
|
@ -24,23 +25,23 @@ func NewRemoteRepository(ms mstore.MStorer) *RemoteRepository {
|
|||
}
|
||||
}
|
||||
|
||||
func (rr *RemoteRepository) FindAll(folder string) ([]*Task, error) {
|
||||
func (rr *RemoteRepository) FindAll(folder string) ([]*task.Task, error) {
|
||||
msgs, err := rr.mstore.Messages(folder)
|
||||
if err != nil {
|
||||
return []*Task{}, fmt.Errorf("%w: %v", ErrMStoreError, err)
|
||||
return []*task.Task{}, fmt.Errorf("%w: %v", ErrMStoreError, err)
|
||||
}
|
||||
|
||||
tasks := []*Task{}
|
||||
tasks := []*task.Task{}
|
||||
for _, msg := range msgs {
|
||||
if msg.Valid() {
|
||||
tasks = append(tasks, NewFromMessage(msg))
|
||||
tasks = append(tasks, task.NewFromMessage(msg))
|
||||
}
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func (rr *RemoteRepository) Update(t *Task) error {
|
||||
func (rr *RemoteRepository) Update(t *task.Task) error {
|
||||
if t == nil {
|
||||
return ErrInvalidTask
|
||||
}
|
||||
|
@ -58,7 +59,7 @@ func (rr *RemoteRepository) Update(t *Task) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (rr *RemoteRepository) Add(t *Task) error {
|
||||
func (rr *RemoteRepository) Add(t *task.Task) error {
|
||||
if t == nil {
|
||||
return ErrInvalidTask
|
||||
}
|
||||
|
@ -80,15 +81,15 @@ func (rr *RemoteRepository) CleanUp() error {
|
|||
}
|
||||
msgsSet := make(map[string][]msgInfo)
|
||||
|
||||
for _, folder := range knownFolders {
|
||||
for _, folder := range task.KnownFolders {
|
||||
msgs, err := rr.mstore.Messages(folder)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrMStoreError, err)
|
||||
}
|
||||
|
||||
for _, msg := range msgs {
|
||||
id, _ := FieldFromBody(FIELD_ID, msg.Body)
|
||||
versionStr, _ := FieldFromBody(FIELD_VERSION, msg.Body)
|
||||
id, _ := task.FieldFromBody(task.FIELD_ID, msg.Body)
|
||||
versionStr, _ := task.FieldFromBody(task.FIELD_VERSION, msg.Body)
|
||||
version, _ := strconv.Atoi(versionStr)
|
||||
if _, ok := msgsSet[id]; !ok {
|
||||
msgsSet[id] = []msgInfo{}
|
|
@ -1,4 +1,4 @@
|
|||
package task_test
|
||||
package storage_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
@ -6,6 +6,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"git.ewintr.nl/go-kit/test"
|
||||
"git.ewintr.nl/gte/internal/storage"
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
"git.ewintr.nl/gte/pkg/mstore"
|
||||
)
|
||||
|
@ -33,7 +34,7 @@ func TestRepoFindAll(t *testing.T) {
|
|||
{
|
||||
name: "unknown folder",
|
||||
folder: "unknown",
|
||||
expErr: task.ErrMStoreError,
|
||||
expErr: storage.ErrMStoreError,
|
||||
},
|
||||
{
|
||||
name: "not empty",
|
||||
|
@ -53,7 +54,7 @@ func TestRepoFindAll(t *testing.T) {
|
|||
for _, task := range tc.tasks {
|
||||
test.OK(t, store.Add(task.Folder, task.Subject, "body"))
|
||||
}
|
||||
repo := task.NewRemoteRepository(store)
|
||||
repo := storage.NewRemoteRepository(store)
|
||||
actTasks, err := repo.FindAll(tc.folder)
|
||||
test.Equals(t, true, errors.Is(err, tc.expErr))
|
||||
if err != nil {
|
||||
|
@ -86,7 +87,7 @@ func TestRepoUpdate(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
name: "nil task",
|
||||
expErr: task.ErrInvalidTask,
|
||||
expErr: storage.ErrInvalidTask,
|
||||
},
|
||||
{
|
||||
name: "task without message",
|
||||
|
@ -95,7 +96,7 @@ func TestRepoUpdate(t *testing.T) {
|
|||
Folder: folder,
|
||||
Action: action,
|
||||
},
|
||||
expErr: task.ErrMStoreError,
|
||||
expErr: storage.ErrMStoreError,
|
||||
},
|
||||
{
|
||||
name: "changed task",
|
||||
|
@ -115,7 +116,7 @@ func TestRepoUpdate(t *testing.T) {
|
|||
test.OK(t, err)
|
||||
test.OK(t, mem.Add(oldMsg.Folder, oldMsg.Subject, oldMsg.Body))
|
||||
|
||||
repo := task.NewRemoteRepository(mem)
|
||||
repo := storage.NewRemoteRepository(mem)
|
||||
|
||||
actErr := repo.Update(tc.task)
|
||||
test.Equals(t, true, errors.Is(actErr, tc.expErr))
|
||||
|
@ -157,7 +158,7 @@ version: %d
|
|||
test.OK(t, mem.Add(folder, subject, body))
|
||||
}
|
||||
|
||||
repo := task.NewRemoteRepository(mem)
|
||||
repo := storage.NewRemoteRepository(mem)
|
||||
test.OK(t, repo.CleanUp())
|
||||
|
||||
expNew := []*mstore.Message{}
|
|
@ -0,0 +1,216 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.ewintr.nl/gte/internal/task"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type sqliteMigration string
|
||||
|
||||
var sqliteMigrations = []sqliteMigration{
|
||||
`CREATE TABLE task ("id" TEXT, "version" INTEGER, "folder" TEXT, "action" TEXT, "project" TEXT, "due" TEXT, "recur" TEXT)`,
|
||||
`CREATE TABLE system ("latest_sync" INTEGER)`,
|
||||
`INSERT INTO system (latest_sync) VALUES (0)`,
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidConfiguration = errors.New("invalid configuration")
|
||||
ErrIncompatibleSQLMigration = errors.New("incompatible migration")
|
||||
ErrNotEnoughSQLMigrations = errors.New("already more migrations than wanted")
|
||||
ErrSqliteFailure = errors.New("sqlite returned an error")
|
||||
)
|
||||
|
||||
type SqliteConfig struct {
|
||||
DBPath string
|
||||
}
|
||||
|
||||
// Sqlite is an sqlite implementation of LocalRepository
|
||||
type Sqlite struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewSqlite(conf *SqliteConfig) (*Sqlite, error) {
|
||||
db, err := sql.Open("sqlite", conf.DBPath)
|
||||
if err != nil {
|
||||
return &Sqlite{}, fmt.Errorf("%w: %v", ErrInvalidConfiguration, err)
|
||||
}
|
||||
|
||||
s := &Sqlite{
|
||||
db: db,
|
||||
}
|
||||
|
||||
if err := s.migrate(sqliteMigrations); err != nil {
|
||||
return &Sqlite{}, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Sqlite) LatestSync() (time.Time, error) {
|
||||
rows, err := s.db.Query(`SELECT latest_sync FROM system`)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
rows.Next()
|
||||
var latest int64
|
||||
if err := rows.Scan(&latest); err != nil {
|
||||
return time.Time{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
|
||||
return time.Unix(latest, 0), nil
|
||||
}
|
||||
|
||||
func (s *Sqlite) SetTasks(tasks []*task.Task) error {
|
||||
if _, err := s.db.Exec(`DELETE FROM task`); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
|
||||
for _, t := range tasks {
|
||||
var recurStr string
|
||||
if t.Recur != nil {
|
||||
recurStr = t.Recur.String()
|
||||
}
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO task
|
||||
(id, version, folder, action, project, due, recur)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?)`,
|
||||
t.Id, t.Version, t.Folder, t.Action, t.Project, t.Due.String(), recurStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := s.db.Exec(`
|
||||
UPDATE system
|
||||
SET latest_sync = ?`,
|
||||
time.Now().Unix()); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Sqlite) FindAllInFolder(folder string) ([]*task.Task, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, version, folder, action, project, due, recur
|
||||
FROM task
|
||||
WHERE folder = ?`, folder)
|
||||
if err != nil {
|
||||
return []*task.Task{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
|
||||
return tasksFromRows(rows)
|
||||
}
|
||||
|
||||
func (s *Sqlite) FindAllInProject(project string) ([]*task.Task, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, version, folder, action, project, due, recur
|
||||
FROM task
|
||||
WHERE project = ?`, project)
|
||||
if err != nil {
|
||||
return []*task.Task{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
|
||||
return tasksFromRows(rows)
|
||||
}
|
||||
|
||||
func tasksFromRows(rows *sql.Rows) ([]*task.Task, error) {
|
||||
tasks := []*task.Task{}
|
||||
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var id, folder, action, project, due, recur string
|
||||
var version int
|
||||
if err := rows.Scan(&id, &version, &folder, &action, &project, &due, &recur); err != nil {
|
||||
return []*task.Task{}, fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
tasks = append(tasks, &task.Task{
|
||||
Id: id,
|
||||
Version: version,
|
||||
Folder: folder,
|
||||
Action: action,
|
||||
Project: project,
|
||||
Due: task.NewDateFromString(due),
|
||||
Recur: task.NewRecurrer(recur),
|
||||
})
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func (s *Sqlite) migrate(wanted []sqliteMigration) error {
|
||||
// admin table
|
||||
if _, err := s.db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS migration
|
||||
("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "query" TEXT)
|
||||
`); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// find existing
|
||||
rows, err := s.db.Query(`SELECT query FROM migration ORDER BY id`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
|
||||
existing := []sqliteMigration{}
|
||||
for rows.Next() {
|
||||
var query string
|
||||
if err := rows.Scan(&query); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
existing = append(existing, sqliteMigration(query))
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
// compare
|
||||
missing, err := compareMigrations(wanted, existing)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
|
||||
// execute missing
|
||||
for _, query := range missing {
|
||||
if _, err := s.db.Exec(string(query)); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
|
||||
// register
|
||||
if _, err := s.db.Exec(`
|
||||
INSERT INTO migration
|
||||
(query) VALUES (?)
|
||||
`, query); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrSqliteFailure, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func compareMigrations(wanted, existing []sqliteMigration) ([]sqliteMigration, error) {
|
||||
needed := []sqliteMigration{}
|
||||
if len(wanted) < len(existing) {
|
||||
return []sqliteMigration{}, ErrNotEnoughSQLMigrations
|
||||
}
|
||||
|
||||
for i, want := range wanted {
|
||||
switch {
|
||||
case i >= len(existing):
|
||||
needed = append(needed, want)
|
||||
case want == existing[i]:
|
||||
// do nothing
|
||||
case want != existing[i]:
|
||||
return []sqliteMigration{}, fmt.Errorf("%w: %v", ErrIncompatibleSQLMigration, want)
|
||||
}
|
||||
}
|
||||
|
||||
return needed, nil
|
||||
}
|
|
@ -37,7 +37,7 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
knownFolders = []string{
|
||||
KnownFolders = []string{
|
||||
FOLDER_INBOX,
|
||||
FOLDER_NEW,
|
||||
FOLDER_RECURRING,
|
||||
|
@ -57,6 +57,9 @@ var (
|
|||
)
|
||||
|
||||
type Task struct {
|
||||
// Message is the underlying message from which the task was created
|
||||
// It only has meaning for remote repositories and will be nil in
|
||||
// local situations. It will be filtered out in LocalRepository.SetTasks()
|
||||
Message *mstore.Message
|
||||
|
||||
Id string
|
||||
|
|
Loading…
Reference in New Issue