9 Commits
v0.0 ... v0.2

Author SHA1 Message Date
bel
ff3ed3a57e gzip 2020-01-26 18:07:28 +00:00
bel
39ca01c3e8 fix 2020-01-21 06:34:26 +00:00
bel
c457cb1daf fix json api 2020-01-21 06:13:03 +00:00
bel
73efeb0acf remove php dependency 2020-01-21 05:38:50 +00:00
bel
afcc33b76b fix show completed 2020-01-20 21:33:44 -07:00
bel
6319c18ddd task sorting might work 2020-01-20 21:18:28 -07:00
bel
cb73169eeb refactor and unit test filters 2020-01-20 16:24:43 -07:00
bel
b47327dfe6 Fix tag filtering 2020-01-20 16:06:33 -07:00
bel
097ca9b8c0 Serve files for static 2020-01-20 16:06:21 -07:00
9 changed files with 224 additions and 108 deletions

View File

@@ -1,16 +1,16 @@
FROM frolvlad/alpine-glibc:alpine-3.9_glibc-2.29
RUN apk update \
&& apk add --no-cache \
ca-certificates \
bash jq curl \
php php-sqlite3 php-pdo php-pdo_mysql php-json php-pdo_sqlite
FROM golang:1.13-alpine as certs
RUN apk update && apk add --no-cache ca-certificates
FROM busybox:glibc
RUN mkdir -p /var/log
WORKDIR /main
COPY --from=certs /etc/ssl/certs /etc/ssl/certs
COPY . .
ENV GOPATH=""
ENV MNT="/mnt/"
ENTRYPOINT ["/bin/bash", "/main/entrypoint.sh"]
ENTRYPOINT ["/main/exec-todo-server"]
CMD []

View File

@@ -13,6 +13,7 @@ var (
StoreAddr string
StoreUser string
StorePass string
Root string
MyTinyTodo string
)
@@ -32,6 +33,7 @@ func Refresh() {
as.Append(args.STRING, "storeuser", "user of store", "")
as.Append(args.STRING, "storepass", "pass of store", "")
as.Append(args.STRING, "mtt", "url of php server", "http://localhost:38808")
as.Append(args.STRING, "root", "root of static files", "./public")
if err := as.Parse(); err != nil {
panic(err)
}
@@ -41,5 +43,6 @@ func Refresh() {
StoreAddr = as.Get("storeaddr").GetString()
StoreUser = as.Get("storeuser").GetString()
StorePass = as.Get("storepass").GetString()
Root = as.Get("root").GetString()
MyTinyTodo = as.Get("mtt").GetString()
}

View File

@@ -1,17 +0,0 @@
#! /bin/bash
(
pushd /main/testdata/mytinytodo*
php -S 0.0.0.0:38808
kill -9 1
) &
(
until curl localhost:39909; do
sleep 1
done
pushd /main/testdata
bash ./migrate.sh 192.168.0.86:44112 localhost:39909
) &
exec /main/exec-todo-server "$@"

View File

@@ -7,41 +7,21 @@ import (
"local/todo-server/server/ajax/form"
"local/todo-server/server/ajax/task"
"net/http"
"sort"
"strconv"
"strings"
)
type taskWithDelta struct {
task *task.Task
delta int
}
func (a *Ajax) loadTasks(w http.ResponseWriter, r *http.Request) error {
listID, _, _ := a.Cur(r)
filterComplete := func(t *task.Task) bool {
if form.Get(r, "compl") == "" {
return true
}
return form.Get(r, "compl") == "1" || !t.Complete
}
filterTags := func(t *task.Task) bool {
if form.Get(r, "t") == "" {
return true
}
whitelistTags := form.ToStrArr(form.Get(r, "t"))
if len(whitelistTags) == 0 {
return true
}
whitelistTagMap := make(map[string]struct{})
for _, tag := range whitelistTags {
whitelistTagMap[tag] = struct{}{}
}
for _, tag := range t.Tags {
if _, ok := whitelistTagMap[tag]; ok {
return true
}
}
return false
}
filterSubstr := func(t *task.Task) bool {
substr := form.Get(r, "s")
return substr == "" || strings.Contains(fmt.Sprintf("%+v", t), substr)
}
filterComplete := filterComplete(form.Get(r, "compl"))
filterTags := filterTags(form.ToStrArr(form.Get(r, "t")))
filterSubstr := filterSubstr(form.Get(r, "s"))
tasks, err := a.storageListTasks(listID, filterComplete, filterTags, filterSubstr)
if err != nil {
return err
@@ -49,6 +29,35 @@ func (a *Ajax) loadTasks(w http.ResponseWriter, r *http.Request) error {
return json.NewEncoder(w).Encode(map[string]interface{}{"list": tasks})
}
func filterComplete(compl string) func(t *task.Task) bool {
return func(t *task.Task) bool {
return compl == "" || !t.Complete || (compl == "1" && t.Complete)
}
}
func filterTags(tags []string) func(t *task.Task) bool {
return func(t *task.Task) bool {
for _, whitelisted := range tags {
found := false
for _, tag := range t.Tags {
if whitelisted == tag {
found = true
}
}
if !found {
return false
}
}
return true
}
}
func filterSubstr(substr string) func(t *task.Task) bool {
return func(t *task.Task) bool {
return substr == "" || strings.Contains(fmt.Sprintf("%+v", t), substr)
}
}
func (a *Ajax) newTask(w http.ResponseWriter, r *http.Request) error {
listID, newTask, err := a.makeTask(r)
if err != nil {
@@ -83,7 +92,10 @@ func (a *Ajax) makeTask(r *http.Request) (string, *task.Task, error) {
func (a *Ajax) deleteTask(w http.ResponseWriter, r *http.Request) error {
_, taskID, _ := a.Cur(r)
return a.storageDelTask(taskID)
if err := a.storageDelTask(taskID); err != nil {
return err
}
return json.NewEncoder(w).Encode(map[string]interface{}{"total": 0})
}
func (a *Ajax) completeTask(w http.ResponseWriter, r *http.Request) error {
@@ -107,45 +119,57 @@ func (a *Ajax) completeTask(w http.ResponseWriter, r *http.Request) error {
func (a *Ajax) editNote(w http.ResponseWriter, r *http.Request) error {
listID, taskID, _ := a.Cur(r)
task, err := a.storageGetTask(taskID)
editedTask, err := a.storageGetTask(taskID)
if err != nil {
return err
}
task.SetNote(form.Get(r, "note"))
return a.storageSetTask(listID, task)
editedTask.SetNote(form.Get(r, "note"))
if err := a.storageSetTask(listID, editedTask); err != nil {
return err
}
return json.NewEncoder(w).Encode(map[string]interface{}{"total": 1, "list": []*task.Task{editedTask}})
}
func (a *Ajax) editTask(w http.ResponseWriter, r *http.Request) error {
listID, task, err := a.makeTask(r)
listID, editedTask, err := a.makeTask(r)
if err != nil {
return err
}
_, taskID, _ := a.Cur(r)
task.UUID = taskID
return a.storageSetTask(listID, task)
editedTask.UUID = taskID
if err := a.storageSetTask(listID, editedTask); err != nil {
return err
}
return json.NewEncoder(w).Encode(map[string]interface{}{"total": 1, "list": []*task.Task{editedTask}})
}
func (a *Ajax) setPrio(w http.ResponseWriter, r *http.Request) error {
listID, taskID, _ := a.Cur(r)
task, err := a.storageGetTask(taskID)
editedTask, err := a.storageGetTask(taskID)
if err != nil {
return err
}
task.SetPrio(form.ToInt(form.Get(r, "prio")))
return a.storageSetTask(listID, task)
editedTask.SetPrio(form.ToInt(form.Get(r, "prio")))
if err := a.storageSetTask(listID, editedTask); err != nil {
return err
}
return json.NewEncoder(w).Encode(map[string]interface{}{"total": 1, "list": []*task.Task{editedTask}})
}
func (a *Ajax) moveTask(w http.ResponseWriter, r *http.Request) error {
_, taskID, _ := a.Cur(r)
toList := form.Get(r, "to")
task, err := a.storageGetTask(taskID)
movedTask, err := a.storageGetTask(taskID)
if err != nil {
return err
}
if err := a.storageDelTask(taskID); err != nil {
return err
}
return a.storageSetTask(toList, task)
if err := a.storageSetTask(toList, movedTask); err != nil {
return err
}
return json.NewEncoder(w).Encode(map[string]interface{}{"total": 1, "list": []*task.Task{movedTask}})
}
func (a *Ajax) parseTaskStr(w http.ResponseWriter, r *http.Request) error {
@@ -155,8 +179,8 @@ func (a *Ajax) parseTaskStr(w http.ResponseWriter, r *http.Request) error {
func (a *Ajax) changeOrder(w http.ResponseWriter, r *http.Request) error {
order := form.Get(r, "order")
orders := strings.Split(order, "&")
sum := 0
zero := ""
modified := make([]taskWithDelta, 0)
indices := make([]int, 0)
for _, order := range orders {
taskIDDelta := strings.Split(order, "=")
if len(taskIDDelta) < 2 {
@@ -164,43 +188,34 @@ func (a *Ajax) changeOrder(w http.ResponseWriter, r *http.Request) error {
}
taskID := taskIDDelta[0]
delta, _ := strconv.Atoi(taskIDDelta[1])
if delta < 0 {
delta = -1
} else {
delta = 0
}
task, err := a.storageGetTask(taskID)
if err != nil {
return err
}
listID, err := a.taskIDToListID(taskID)
modified = append(modified, taskWithDelta{task: task, delta: delta})
indices = append(indices, task.Index)
}
sort.Slice(modified, func(i, j int) bool {
if modified[i].delta < modified[j].delta {
return true
} else if modified[i].delta > modified[j].delta {
return false
}
return modified[i].task.Index < modified[j].task.Index
})
sort.Ints(indices)
for i := 0; i < len(modified); i++ {
task := *modified[i].task
task.Index = indices[i]
listID, err := a.taskIDToListID(task.UUID)
if err != nil {
return err
}
task.Index += delta
if err := a.storageSetTask(listID, task); err != nil {
if err := a.storageSetTask(listID, &task); err != nil {
return err
}
if delta == 0 {
zero = taskID
}
sum += delta
}
if zero == "" || sum == 0 {
return nil
}
task, err := a.storageGetTask(zero)
if err != nil {
return err
}
listID, err := a.taskIDToListID(zero)
if err != nil {
return err
}
task.Index -= sum
if err := a.storageSetTask(listID, task); err != nil {
return err
}
fmt.Fprint(w, `{"total":1}`)
fmt.Fprintf(w, `{"total":1}`)
return nil
}

View File

@@ -2,6 +2,7 @@ package ajax
import (
"encoding/json"
"local/todo-server/server/ajax/form"
"local/todo-server/server/ajax/task"
"net/http"
"net/http/httptest"
@@ -207,3 +208,49 @@ func TestAjaxChangeOrder(t *testing.T) {
t.Error(tasks[2])
}
}
func TestFilterComplete(t *testing.T) {
cases := []struct {
query string
task bool
out bool
}{
{query: "", out: true, task: false},
{query: "0", out: true, task: false},
{query: "1", out: true, task: false},
}
for name, c := range cases {
task := &task.Task{Complete: c.task}
out := filterComplete(c.query)
if out(task) != c.out {
t.Errorf("[%d] want %v, got %v", name, c.out, out(task))
}
}
}
func TestFilterTags(t *testing.T) {
cases := map[string]struct {
query string
tags []string
out bool
}{
"no filter": {query: "", out: true},
"single matching filter": {query: "a", out: true, tags: []string{"a"}},
"single non-matching filter": {query: "a", out: false, tags: []string{"b"}},
"duo matching filter": {query: "a, b", out: true, tags: []string{"b", "a"}},
"duo partial-matching filter": {query: "a, c", out: false, tags: []string{"b", "a"}},
"duo non-matching filter": {query: "a, c", out: false, tags: []string{"d", "e"}},
"trio matching filter": {query: "a, b, c", out: true, tags: []string{"b", "a", "c", "d", "e"}},
"trio partial-matching filter": {query: "a, c, d", out: false, tags: []string{"b", "a", "d"}},
"trio non-matching filter": {query: "a, b, c", out: false, tags: []string{"x", "y", "z"}},
}
for name, c := range cases {
task := &task.Task{Tags: c.tags}
out := filterTags(form.ToStrArr(c.query))
if v := out(task); v != c.out {
t.Errorf("[%s] want %v, got %v", name, c.out, v)
}
}
}

View File

@@ -3,14 +3,15 @@ package server
import (
"fmt"
"io"
"local/gziphttp"
"local/router"
"local/todo-server/config"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path"
"path/filepath"
)
func (s *Server) Routes() error {
@@ -19,12 +20,20 @@ func (s *Server) Routes() error {
handler http.HandlerFunc
}{
{
path: fmt.Sprintf("%s%s", router.Wildcard, router.Wildcard),
handler: s.phpProxy,
path: "/",
handler: s.gzip(s.index),
},
{
path: fmt.Sprintf("ajax.php"),
handler: s.HandleAjax,
path: "/mytinytodo_lang.php",
handler: s.gzip(s.lang),
},
{
path: fmt.Sprintf("%s%s", router.Wildcard, router.Wildcard),
handler: s.gzip(s.phpProxy),
},
{
path: "/ajax.php",
handler: s.gzip(s.HandleAjax),
},
}
@@ -36,10 +45,39 @@ func (s *Server) Routes() error {
return nil
}
func (s *Server) lang(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `
mytinytodo.lang.init({
confirmDelete: "Are you sure you want to delete the task?",
confirmLeave: "There can be unsaved data. Do you really want to leave?",
actionNoteSave: "save",
actionNoteCancel: "cancel",
error: "Some error occurred (click for details)",
denied: "Access denied",
invalidpass: "Wrong password",
tagfilter: "Tag:",
addList: "Create new list",
addListDefault: "Todo",
renameList: "Rename list",
deleteList: "This will delete current list with all tasks in it.\nAre you sure?",
clearCompleted: "This will delete all completed tasks in the list.\nAre you sure?",
settingsSaved: "Settings saved. Reloading...",
daysMin: ["Su","Mo","Tu","We","Th","Fr","Sa"],
daysLong: ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],
monthsLong: ["January","February","March","April","May","June","July","August","September","October","November","December"],
tags: "Tags",
tasks: "Tasks",
f_past: "Overdue",
f_today: "Today and tomorrow",
f_soon: "Soon"
});
`)
}
func (s *Server) index(w http.ResponseWriter, r *http.Request) {
f, err := os.Open(path.Join(config.MyTinyTodo, "index.php"))
f, err := os.Open(path.Join(config.Root, "index.html"))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer f.Close()
@@ -47,11 +85,36 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) phpProxy(w http.ResponseWriter, r *http.Request) {
switch filepath.Ext(r.URL.Path) {
case ".php":
default:
s.static(w, r)
return
}
url, err := url.Parse(config.MyTinyTodo)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
proxy := httputil.NewSingleHostReverseProxy(url)
proxy.ServeHTTP(w, r)
log.Println("proxying", url.String(), r.URL.Path)
//proxy := httputil.NewSingleHostReverseProxy(url)
//proxy.ServeHTTP(w, r)
}
}
func (s *Server) static(w http.ResponseWriter, r *http.Request) {
s.fileServer.ServeHTTP(w, r)
}
func (s *Server) gzip(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if gziphttp.Can(r) {
gz := gziphttp.New(w)
defer gz.Close()
w = gz
}
if filepath.Ext(r.URL.Path) == ".css" {
w.Header().Set("Content-Type", "text/css; charset=utf-8")
}
h(w, r)
}
}

View File

@@ -2,12 +2,15 @@ package server
import (
"local/router"
"local/todo-server/config"
"local/todo-server/server/ajax"
"net/http"
)
type Server struct {
*ajax.Ajax
*router.Router
fileServer http.Handler
}
func New() *Server {
@@ -15,8 +18,10 @@ func New() *Server {
if err != nil {
panic(err)
}
fileServer := http.FileServer(http.Dir(config.Root))
return &Server{
Ajax: ajax,
Router: router.New(),
fileServer: fileServer,
}
}

0
source_to_run_loaded.sh Normal file → Executable file
View File

2
testdata/migrate.sh vendored Normal file → Executable file
View File

@@ -114,6 +114,6 @@ function b64decode() {
}
if [ "$0" == "$BASH_SOURCE" ]; then
echo bash migrate.sh 192.168.0.86:44112 localhost:39909
echo bash migrate.sh 192.168.0.86:44112 localhost:38809
time main "$@"
fi