local client with sync and today
This commit is contained in:
parent
4b6167cbd0
commit
d82f4b7c0a
|
@ -2,4 +2,5 @@
|
||||||
/gte-generate-recurring
|
/gte-generate-recurring
|
||||||
/gte
|
/gte
|
||||||
/gte-daemon
|
/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/go-kit/log"
|
||||||
"git.ewintr.nl/gte/internal/configuration"
|
"git.ewintr.nl/gte/internal/configuration"
|
||||||
"git.ewintr.nl/gte/internal/process"
|
"git.ewintr.nl/gte/internal/process"
|
||||||
|
"git.ewintr.nl/gte/internal/storage"
|
||||||
"git.ewintr.nl/gte/internal/task"
|
"git.ewintr.nl/gte/internal/task"
|
||||||
"git.ewintr.nl/gte/pkg/msend"
|
"git.ewintr.nl/gte/pkg/msend"
|
||||||
"git.ewintr.nl/gte/pkg/mstore"
|
"git.ewintr.nl/gte/pkg/mstore"
|
||||||
|
@ -35,8 +36,8 @@ func main() {
|
||||||
|
|
||||||
msgStore := mstore.NewIMAP(config.IMAP())
|
msgStore := mstore.NewIMAP(config.IMAP())
|
||||||
mailSend := msend.NewSSLSMTP(config.SMTP())
|
mailSend := msend.NewSSLSMTP(config.SMTP())
|
||||||
repo := task.NewRemoteRepository(msgStore)
|
repo := storage.NewRemoteRepository(msgStore)
|
||||||
disp := task.NewDispatcher(mailSend)
|
disp := storage.NewDispatcher(mailSend)
|
||||||
|
|
||||||
inboxProc := process.NewInbox(repo)
|
inboxProc := process.NewInbox(repo)
|
||||||
recurProc := process.NewRecur(repo, disp, *daysAhead)
|
recurProc := process.NewRecur(repo, disp, *daysAhead)
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"git.ewintr.nl/go-kit/log"
|
"git.ewintr.nl/go-kit/log"
|
||||||
"git.ewintr.nl/gte/internal/configuration"
|
"git.ewintr.nl/gte/internal/configuration"
|
||||||
"git.ewintr.nl/gte/internal/process"
|
"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/msend"
|
||||||
"git.ewintr.nl/gte/pkg/mstore"
|
"git.ewintr.nl/gte/pkg/mstore"
|
||||||
)
|
)
|
||||||
|
@ -28,8 +28,8 @@ func main() {
|
||||||
|
|
||||||
msgStore := mstore.NewIMAP(config.IMAP())
|
msgStore := mstore.NewIMAP(config.IMAP())
|
||||||
mailSend := msend.NewSSLSMTP(config.SMTP())
|
mailSend := msend.NewSSLSMTP(config.SMTP())
|
||||||
taskRepo := task.NewRemoteRepository(msgStore)
|
taskRepo := storage.NewRemoteRepository(msgStore)
|
||||||
taskDisp := task.NewDispatcher(mailSend)
|
taskDisp := storage.NewDispatcher(mailSend)
|
||||||
recur := process.NewRecur(taskRepo, taskDisp, *daysAhead)
|
recur := process.NewRecur(taskRepo, taskDisp, *daysAhead)
|
||||||
|
|
||||||
result, err := recur.Process()
|
result, err := recur.Process()
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"git.ewintr.nl/go-kit/log"
|
"git.ewintr.nl/go-kit/log"
|
||||||
"git.ewintr.nl/gte/internal/configuration"
|
"git.ewintr.nl/gte/internal/configuration"
|
||||||
"git.ewintr.nl/gte/internal/process"
|
"git.ewintr.nl/gte/internal/process"
|
||||||
"git.ewintr.nl/gte/internal/task"
|
"git.ewintr.nl/gte/internal/storage"
|
||||||
"git.ewintr.nl/gte/pkg/mstore"
|
"git.ewintr.nl/gte/pkg/mstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ func main() {
|
||||||
}
|
}
|
||||||
config := configuration.New(configFile)
|
config := configuration.New(configFile)
|
||||||
msgStore := mstore.NewIMAP(config.IMAP())
|
msgStore := mstore.NewIMAP(config.IMAP())
|
||||||
inboxProcessor := process.NewInbox(task.NewRemoteRepository(msgStore))
|
inboxProcessor := process.NewInbox(storage.NewRemoteRepository(msgStore))
|
||||||
|
|
||||||
result, err := inboxProcessor.Process()
|
result, err := inboxProcessor.Process()
|
||||||
if err != nil {
|
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-imap v1.1.0
|
||||||
github.com/emersion/go-message v0.14.1
|
github.com/emersion/go-message v0.14.1
|
||||||
github.com/google/uuid v1.2.0
|
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"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.ewintr.nl/gte/internal/storage"
|
||||||
"git.ewintr.nl/gte/pkg/msend"
|
"git.ewintr.nl/gte/pkg/msend"
|
||||||
"git.ewintr.nl/gte/pkg/mstore"
|
"git.ewintr.nl/gte/pkg/mstore"
|
||||||
)
|
)
|
||||||
|
@ -28,6 +30,12 @@ type Configuration struct {
|
||||||
|
|
||||||
ToName string
|
ToName string
|
||||||
ToAddress string
|
ToAddress string
|
||||||
|
|
||||||
|
LocalDBPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocalConfiguration struct {
|
||||||
|
MinSyncInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(src io.Reader) *Configuration {
|
func New(src io.Reader) *Configuration {
|
||||||
|
@ -62,6 +70,8 @@ func New(src io.Reader) *Configuration {
|
||||||
conf.FromName = value
|
conf.FromName = value
|
||||||
case "from_address":
|
case "from_address":
|
||||||
conf.FromAddress = value
|
conf.FromAddress = value
|
||||||
|
case "local_db_path":
|
||||||
|
conf.LocalDBPath = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,3 +95,9 @@ func (c *Configuration) SMTP() *msend.SSLSMTPConfig {
|
||||||
To: c.ToAddress,
|
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/go-kit/test"
|
||||||
"git.ewintr.nl/gte/internal/configuration"
|
"git.ewintr.nl/gte/internal/configuration"
|
||||||
|
"git.ewintr.nl/gte/internal/storage"
|
||||||
"git.ewintr.nl/gte/pkg/msend"
|
"git.ewintr.nl/gte/pkg/msend"
|
||||||
"git.ewintr.nl/gte/pkg/mstore"
|
"git.ewintr.nl/gte/pkg/mstore"
|
||||||
)
|
)
|
||||||
|
@ -67,6 +68,13 @@ func TestNew(t *testing.T) {
|
||||||
FromAddress: "from_address",
|
FromAddress: "from_address",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "local",
|
||||||
|
source: "local_db_path=path",
|
||||||
|
exp: &configuration.Configuration{
|
||||||
|
LocalDBPath: "path",
|
||||||
|
},
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
test.Equals(t, tc.exp, configuration.New(strings.NewReader(tc.source)))
|
test.Equals(t, tc.exp, configuration.New(strings.NewReader(tc.source)))
|
||||||
|
@ -86,6 +94,7 @@ func TestConfigs(t *testing.T) {
|
||||||
ToAddress: "to_address",
|
ToAddress: "to_address",
|
||||||
FromName: "from_name",
|
FromName: "from_name",
|
||||||
FromAddress: "from_address",
|
FromAddress: "from_address",
|
||||||
|
LocalDBPath: "db_path",
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("imap", func(t *testing.T) {
|
t.Run("imap", func(t *testing.T) {
|
||||||
|
@ -108,4 +117,11 @@ func TestConfigs(t *testing.T) {
|
||||||
}
|
}
|
||||||
test.Equals(t, exp, conf.SMTP())
|
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"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.ewintr.nl/gte/internal/storage"
|
||||||
"git.ewintr.nl/gte/internal/task"
|
"git.ewintr.nl/gte/internal/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,8 +16,9 @@ var (
|
||||||
inboxLock sync.Mutex
|
inboxLock sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Inbox processes all messages in INBOX in a remote repository
|
||||||
type Inbox struct {
|
type Inbox struct {
|
||||||
taskRepo *task.RemoteRepository
|
taskRepo *storage.RemoteRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
type InboxResult struct {
|
type InboxResult struct {
|
||||||
|
@ -24,7 +26,7 @@ type InboxResult struct {
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInbox(repo *task.RemoteRepository) *Inbox {
|
func NewInbox(repo *storage.RemoteRepository) *Inbox {
|
||||||
return &Inbox{
|
return &Inbox{
|
||||||
taskRepo: repo,
|
taskRepo: repo,
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
"git.ewintr.nl/go-kit/test"
|
"git.ewintr.nl/go-kit/test"
|
||||||
"git.ewintr.nl/gte/internal/process"
|
"git.ewintr.nl/gte/internal/process"
|
||||||
|
"git.ewintr.nl/gte/internal/storage"
|
||||||
"git.ewintr.nl/gte/internal/task"
|
"git.ewintr.nl/gte/internal/task"
|
||||||
"git.ewintr.nl/gte/pkg/mstore"
|
"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()
|
actResult, err := inboxProc.Process()
|
||||||
|
|
||||||
test.OK(t, err)
|
test.OK(t, err)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.ewintr.nl/gte/internal/storage"
|
||||||
"git.ewintr.nl/gte/internal/task"
|
"git.ewintr.nl/gte/internal/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,9 +16,10 @@ var (
|
||||||
recurLock sync.Mutex
|
recurLock sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Recur generates new tasks from a recurring task for a given day
|
||||||
type Recur struct {
|
type Recur struct {
|
||||||
taskRepo *task.RemoteRepository
|
taskRepo *storage.RemoteRepository
|
||||||
taskDispatcher *task.Dispatcher
|
taskDispatcher *storage.Dispatcher
|
||||||
daysAhead int
|
daysAhead int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +28,7 @@ type RecurResult struct {
|
||||||
Count int `json:"count"`
|
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{
|
return &Recur{
|
||||||
taskRepo: repo,
|
taskRepo: repo,
|
||||||
taskDispatcher: disp,
|
taskDispatcher: disp,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
"git.ewintr.nl/go-kit/test"
|
"git.ewintr.nl/go-kit/test"
|
||||||
"git.ewintr.nl/gte/internal/process"
|
"git.ewintr.nl/gte/internal/process"
|
||||||
|
"git.ewintr.nl/gte/internal/storage"
|
||||||
"git.ewintr.nl/gte/internal/task"
|
"git.ewintr.nl/gte/internal/task"
|
||||||
"git.ewintr.nl/gte/pkg/msend"
|
"git.ewintr.nl/gte/pkg/msend"
|
||||||
"git.ewintr.nl/gte/pkg/mstore"
|
"git.ewintr.nl/gte/pkg/mstore"
|
||||||
|
@ -54,7 +55,7 @@ func TestRecurProcess(t *testing.T) {
|
||||||
}
|
}
|
||||||
msender := msend.NewMemory()
|
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()
|
actResult, err := recurProc.Process()
|
||||||
test.OK(t, err)
|
test.OK(t, err)
|
||||||
test.Equals(t, tc.expCount, actResult.Count)
|
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 {
|
type Dispatcher struct {
|
||||||
msender msend.MSender
|
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{
|
return d.msender.Send(&msend.Message{
|
||||||
Subject: t.FormatSubject(),
|
Subject: t.FormatSubject(),
|
||||||
Body: t.FormatBody(),
|
Body: t.FormatBody(),
|
|
@ -1,17 +1,18 @@
|
||||||
package task_test
|
package storage_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.ewintr.nl/go-kit/test"
|
"git.ewintr.nl/go-kit/test"
|
||||||
|
"git.ewintr.nl/gte/internal/storage"
|
||||||
"git.ewintr.nl/gte/internal/task"
|
"git.ewintr.nl/gte/internal/task"
|
||||||
"git.ewintr.nl/gte/pkg/msend"
|
"git.ewintr.nl/gte/pkg/msend"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDispatcherDispatch(t *testing.T) {
|
func TestDispatcherDispatch(t *testing.T) {
|
||||||
mem := msend.NewMemory()
|
mem := msend.NewMemory()
|
||||||
disp := task.NewDispatcher(mem)
|
disp := storage.NewDispatcher(mem)
|
||||||
tsk := &task.Task{
|
tsk := &task.Task{
|
||||||
Id: "id",
|
Id: "id",
|
||||||
Version: 3,
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"git.ewintr.nl/gte/internal/task"
|
||||||
"git.ewintr.nl/gte/pkg/mstore"
|
"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)
|
msgs, err := rr.mstore.Messages(folder)
|
||||||
if err != nil {
|
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 {
|
for _, msg := range msgs {
|
||||||
if msg.Valid() {
|
if msg.Valid() {
|
||||||
tasks = append(tasks, NewFromMessage(msg))
|
tasks = append(tasks, task.NewFromMessage(msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tasks, nil
|
return tasks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rr *RemoteRepository) Update(t *Task) error {
|
func (rr *RemoteRepository) Update(t *task.Task) error {
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return ErrInvalidTask
|
return ErrInvalidTask
|
||||||
}
|
}
|
||||||
|
@ -58,7 +59,7 @@ func (rr *RemoteRepository) Update(t *Task) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rr *RemoteRepository) Add(t *Task) error {
|
func (rr *RemoteRepository) Add(t *task.Task) error {
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return ErrInvalidTask
|
return ErrInvalidTask
|
||||||
}
|
}
|
||||||
|
@ -80,15 +81,15 @@ func (rr *RemoteRepository) CleanUp() error {
|
||||||
}
|
}
|
||||||
msgsSet := make(map[string][]msgInfo)
|
msgsSet := make(map[string][]msgInfo)
|
||||||
|
|
||||||
for _, folder := range knownFolders {
|
for _, folder := range task.KnownFolders {
|
||||||
msgs, err := rr.mstore.Messages(folder)
|
msgs, err := rr.mstore.Messages(folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %v", ErrMStoreError, err)
|
return fmt.Errorf("%w: %v", ErrMStoreError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, msg := range msgs {
|
for _, msg := range msgs {
|
||||||
id, _ := FieldFromBody(FIELD_ID, msg.Body)
|
id, _ := task.FieldFromBody(task.FIELD_ID, msg.Body)
|
||||||
versionStr, _ := FieldFromBody(FIELD_VERSION, msg.Body)
|
versionStr, _ := task.FieldFromBody(task.FIELD_VERSION, msg.Body)
|
||||||
version, _ := strconv.Atoi(versionStr)
|
version, _ := strconv.Atoi(versionStr)
|
||||||
if _, ok := msgsSet[id]; !ok {
|
if _, ok := msgsSet[id]; !ok {
|
||||||
msgsSet[id] = []msgInfo{}
|
msgsSet[id] = []msgInfo{}
|
|
@ -1,4 +1,4 @@
|
||||||
package task_test
|
package storage_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -6,6 +6,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.ewintr.nl/go-kit/test"
|
"git.ewintr.nl/go-kit/test"
|
||||||
|
"git.ewintr.nl/gte/internal/storage"
|
||||||
"git.ewintr.nl/gte/internal/task"
|
"git.ewintr.nl/gte/internal/task"
|
||||||
"git.ewintr.nl/gte/pkg/mstore"
|
"git.ewintr.nl/gte/pkg/mstore"
|
||||||
)
|
)
|
||||||
|
@ -33,7 +34,7 @@ func TestRepoFindAll(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "unknown folder",
|
name: "unknown folder",
|
||||||
folder: "unknown",
|
folder: "unknown",
|
||||||
expErr: task.ErrMStoreError,
|
expErr: storage.ErrMStoreError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "not empty",
|
name: "not empty",
|
||||||
|
@ -53,7 +54,7 @@ func TestRepoFindAll(t *testing.T) {
|
||||||
for _, task := range tc.tasks {
|
for _, task := range tc.tasks {
|
||||||
test.OK(t, store.Add(task.Folder, task.Subject, "body"))
|
test.OK(t, store.Add(task.Folder, task.Subject, "body"))
|
||||||
}
|
}
|
||||||
repo := task.NewRemoteRepository(store)
|
repo := storage.NewRemoteRepository(store)
|
||||||
actTasks, err := repo.FindAll(tc.folder)
|
actTasks, err := repo.FindAll(tc.folder)
|
||||||
test.Equals(t, true, errors.Is(err, tc.expErr))
|
test.Equals(t, true, errors.Is(err, tc.expErr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -86,7 +87,7 @@ func TestRepoUpdate(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "nil task",
|
name: "nil task",
|
||||||
expErr: task.ErrInvalidTask,
|
expErr: storage.ErrInvalidTask,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "task without message",
|
name: "task without message",
|
||||||
|
@ -95,7 +96,7 @@ func TestRepoUpdate(t *testing.T) {
|
||||||
Folder: folder,
|
Folder: folder,
|
||||||
Action: action,
|
Action: action,
|
||||||
},
|
},
|
||||||
expErr: task.ErrMStoreError,
|
expErr: storage.ErrMStoreError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "changed task",
|
name: "changed task",
|
||||||
|
@ -115,7 +116,7 @@ func TestRepoUpdate(t *testing.T) {
|
||||||
test.OK(t, err)
|
test.OK(t, err)
|
||||||
test.OK(t, mem.Add(oldMsg.Folder, oldMsg.Subject, oldMsg.Body))
|
test.OK(t, mem.Add(oldMsg.Folder, oldMsg.Subject, oldMsg.Body))
|
||||||
|
|
||||||
repo := task.NewRemoteRepository(mem)
|
repo := storage.NewRemoteRepository(mem)
|
||||||
|
|
||||||
actErr := repo.Update(tc.task)
|
actErr := repo.Update(tc.task)
|
||||||
test.Equals(t, true, errors.Is(actErr, tc.expErr))
|
test.Equals(t, true, errors.Is(actErr, tc.expErr))
|
||||||
|
@ -157,7 +158,7 @@ version: %d
|
||||||
test.OK(t, mem.Add(folder, subject, body))
|
test.OK(t, mem.Add(folder, subject, body))
|
||||||
}
|
}
|
||||||
|
|
||||||
repo := task.NewRemoteRepository(mem)
|
repo := storage.NewRemoteRepository(mem)
|
||||||
test.OK(t, repo.CleanUp())
|
test.OK(t, repo.CleanUp())
|
||||||
|
|
||||||
expNew := []*mstore.Message{}
|
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 (
|
var (
|
||||||
knownFolders = []string{
|
KnownFolders = []string{
|
||||||
FOLDER_INBOX,
|
FOLDER_INBOX,
|
||||||
FOLDER_NEW,
|
FOLDER_NEW,
|
||||||
FOLDER_RECURRING,
|
FOLDER_RECURRING,
|
||||||
|
@ -57,6 +57,9 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Task struct {
|
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
|
Message *mstore.Message
|
||||||
|
|
||||||
Id string
|
Id string
|
||||||
|
|
Loading…
Reference in New Issue