Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion backend/controllers/get_tasks.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package controllers

import (
"ccsync_backend/models"
"ccsync_backend/utils"
"ccsync_backend/utils/tw"
"encoding/json"
"net/http"
Expand Down Expand Up @@ -35,10 +37,14 @@ func TasksHandler(w http.ResponseWriter, r *http.Request) {

if r.Method == http.MethodGet {
tasks, err := tw.FetchTasksFromTaskwarrior(email, encryptionSecret, origin, UUID)
if err != nil || tasks == nil {
if err != nil {
utils.Logger.Errorf("Failed to fetch tasks: %v", err)
http.Error(w, "Failed to fetch tasks at backend", http.StatusInternalServerError)
return
}
if tasks == nil {
tasks = []models.Task{}
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tasks)
Expand Down
39 changes: 38 additions & 1 deletion backend/utils/exec_command.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package utils

import "os/exec"
import (
"os"
"os/exec"
"path/filepath"
"strings"
)

func ExecCommandInDir(dir, command string, args ...string) error {
cmd := exec.Command(command, args...)
Expand All @@ -18,3 +23,35 @@ func ExecCommandForOutputInDir(dir, command string, args ...string) ([]byte, err
cmd.Dir = dir
return cmd.Output()
}

// TaskwarriorEnv returns a copy of the process environment with TASKDATA and
// TASKRC pointing at tempDir so Taskwarrior does not use /root/.task.
func TaskwarriorEnv(tempDir string) []string {
env := os.Environ()
filtered := make([]string, 0, len(env)+2)
for _, e := range env {
if strings.HasPrefix(e, "TASKDATA=") || strings.HasPrefix(e, "TASKRC=") {
continue
}
filtered = append(filtered, e)
}
filtered = append(filtered,
"TASKDATA="+tempDir,
"TASKRC="+filepath.Join(tempDir, "taskrc"),
)
return filtered
}

func ExecTaskInDir(dir string, args ...string) error {
cmd := exec.Command("task", args...)
cmd.Dir = dir
cmd.Env = TaskwarriorEnv(dir)
return cmd.Run()
}

func ExecTaskOutputInDir(dir string, args ...string) ([]byte, error) {
cmd := exec.Command("task", args...)
cmd.Dir = dir
cmd.Env = TaskwarriorEnv(dir)
return cmd.Output()
}
13 changes: 6 additions & 7 deletions backend/utils/tw/add_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import (
)

func AddTaskToTaskwarrior(req models.AddTaskRequestBody, dueDate string) error {
if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil {
return fmt.Errorf("error deleting Taskwarrior data: %v", err)
}
taskwarriorMu.Lock()
defer taskwarriorMu.Unlock()

tempDir, err := os.MkdirTemp("", utils.SafeTempDirPrefix("taskwarrior-", req.Email))
if err != nil {
Expand Down Expand Up @@ -75,13 +74,13 @@ func AddTaskToTaskwarrior(req models.AddTaskRequestBody, dueDate string) error {
}
}

if err := utils.ExecCommandInDir(tempDir, "task", cmdArgs...); err != nil {
if err := utils.ExecTaskInDir(tempDir, cmdArgs...); err != nil {
return fmt.Errorf("failed to add task: %v\n %v", err, cmdArgs)
}

var taskID string
if req.End != "" || len(req.Annotations) > 0 {
output, err := utils.ExecCommandForOutputInDir(tempDir, "task", "+LATEST", "_ids")
output, err := utils.ExecTaskOutputInDir(tempDir, "+LATEST", "_ids")
if err != nil {
return fmt.Errorf("failed to get latest task Id: %v", err)
}
Expand All @@ -95,7 +94,7 @@ func AddTaskToTaskwarrior(req models.AddTaskRequestBody, dueDate string) error {
return fmt.Errorf("unexpected end date format error: %v", err)
}
doneArgs := []string{"rc.confirmation=off", taskID, "done", "end:" + end}
if err := utils.ExecCommandInDir(tempDir, "task", doneArgs...); err != nil {
if err := utils.ExecTaskInDir(tempDir, doneArgs...); err != nil {
return fmt.Errorf("failed to complete task with end date: %v", err)
}
}
Expand All @@ -104,7 +103,7 @@ func AddTaskToTaskwarrior(req models.AddTaskRequestBody, dueDate string) error {
for _, annotation := range req.Annotations {
if annotation.Description != "" {
annotateArgs := []string{"rc.confirmation=off", taskID, "annotate", annotation.Description}
if err := utils.ExecCommandInDir(tempDir, "task", annotateArgs...); err != nil {
if err := utils.ExecTaskInDir(tempDir, annotateArgs...); err != nil {
return fmt.Errorf("failed to add annotation to task %s: %v", taskID, err)
}
}
Expand Down
8 changes: 4 additions & 4 deletions backend/utils/tw/complete_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
)

func CompleteTaskInTaskwarrior(email, encryptionSecret, uuid, taskuuid string) error {
if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil {
return fmt.Errorf("error deleting Taskwarrior data: %v", err)
}
taskwarriorMu.Lock()
defer taskwarriorMu.Unlock()

tempDir, err := os.MkdirTemp("", utils.SafeTempDirPrefix("taskwarrior-", email))
if err != nil {
return fmt.Errorf("failed to create temporary directory: %v", err)
Expand All @@ -25,7 +25,7 @@ func CompleteTaskInTaskwarrior(email, encryptionSecret, uuid, taskuuid string) e
return err
}

if err := utils.ExecCommandInDir(tempDir, "task", taskuuid, "done", "rc.confirmation=off"); err != nil {
if err := utils.ExecTaskInDir(tempDir, taskuuid, "done", "rc.confirmation=off"); err != nil {
return fmt.Errorf("failed to mark task as done: %v", err)
}

Expand Down
7 changes: 3 additions & 4 deletions backend/utils/tw/complete_tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import (
func CompleteTasksInTaskwarrior(email, encryptionSecret, uuid string, taskUUIDs []string) (map[string]string, error) {
failedTasks := make(map[string]string)

if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil {
return nil, fmt.Errorf("error deleting Taskwarrior data: %v", err)
}
taskwarriorMu.Lock()
defer taskwarriorMu.Unlock()

tempDir, err := os.MkdirTemp("", utils.SafeTempDirPrefix("taskwarrior-", email))

Expand All @@ -30,7 +29,7 @@ func CompleteTasksInTaskwarrior(email, encryptionSecret, uuid string, taskUUIDs
}

for _, taskuuid := range taskUUIDs {
if err := utils.ExecCommandInDir(tempDir, "task", taskuuid, "done", "rc.confirmation=off"); err != nil {
if err := utils.ExecTaskInDir(tempDir, taskuuid, "done", "rc.confirmation=off"); err != nil {
failedTasks[taskuuid] = err.Error()
continue
}
Expand Down
8 changes: 4 additions & 4 deletions backend/utils/tw/delete_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
)

func DeleteTaskInTaskwarrior(email, encryptionSecret, uuid, taskuuid string) error {
if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil {
return fmt.Errorf("error deleting Taskwarrior data: %v", err)
}
taskwarriorMu.Lock()
defer taskwarriorMu.Unlock()

tempDir, err := os.MkdirTemp("", utils.SafeTempDirPrefix("taskwarrior-", email))
if err != nil {
return fmt.Errorf("failed to create temporary directory: %v", err)
Expand All @@ -25,7 +25,7 @@ func DeleteTaskInTaskwarrior(email, encryptionSecret, uuid, taskuuid string) err
return err
}

if err := utils.ExecCommandInDir(tempDir, "task", taskuuid, "delete", "rc.confirmation=off"); err != nil {
if err := utils.ExecTaskInDir(tempDir, taskuuid, "delete", "rc.confirmation=off"); err != nil {
return fmt.Errorf("failed to mark task as deleted: %v", err)
}

Expand Down
7 changes: 3 additions & 4 deletions backend/utils/tw/delete_tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import (
func DeleteTasksInTaskwarrior(email, encryptionSecret, uuid string, taskUUIDs []string) (map[string]string, error) {
failedTasks := make(map[string]string)

if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil {
return nil, fmt.Errorf("error deleting Taskwarrior data: %v", err)
}
taskwarriorMu.Lock()
defer taskwarriorMu.Unlock()

tempDir, err := os.MkdirTemp("", utils.SafeTempDirPrefix("taskwarrior-", email))

Expand All @@ -30,7 +29,7 @@ func DeleteTasksInTaskwarrior(email, encryptionSecret, uuid string, taskUUIDs []
}

for _, taskuuid := range taskUUIDs {
if err := utils.ExecCommandInDir(tempDir, "task", taskuuid, "delete", "rc.confirmation=off"); err != nil {
if err := utils.ExecTaskInDir(tempDir, taskuuid, "delete", "rc.confirmation=off"); err != nil {
failedTasks[taskuuid] = err.Error()
continue
}
Expand Down
11 changes: 7 additions & 4 deletions backend/utils/tw/edit_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ func EditTaskInTaskwarrior(
tags, depends []string,
annotations []models.Annotation,
) error {
taskwarriorMu.Lock()
defer taskwarriorMu.Unlock()

tempDir, err := os.MkdirTemp("", utils.SafeTempDirPrefix("taskwarrior-", email))
if err != nil {
return fmt.Errorf("failed to create temporary directory: %v", err)
Expand Down Expand Up @@ -84,20 +87,20 @@ func EditTaskInTaskwarrior(
}
}

if err := utils.ExecCommand("task", modifyArgs...); err != nil {
if err := utils.ExecTaskInDir(tempDir, modifyArgs...); err != nil {
return fmt.Errorf("failed to edit task: %v", err)
}

if len(annotations) > 0 {
output, err := utils.ExecCommandForOutputInDir(tempDir, "task", taskUUID, "export")
output, err := utils.ExecTaskOutputInDir(tempDir, taskUUID, "export")
if err == nil {
var tasks []map[string]interface{}
if err := json.Unmarshal(output, &tasks); err == nil && len(tasks) > 0 {
if existingAnnotations, ok := tasks[0]["annotations"].([]interface{}); ok {
for _, ann := range existingAnnotations {
if annMap, ok := ann.(map[string]interface{}); ok {
if desc, ok := annMap["description"].(string); ok {
utils.ExecCommand("task", taskUUID, "denotate", desc)
utils.ExecTaskInDir(tempDir, taskUUID, "denotate", desc)
}
}
}
Expand All @@ -107,7 +110,7 @@ func EditTaskInTaskwarrior(

for _, annotation := range annotations {
if annotation.Description != "" {
if err := utils.ExecCommand("task", taskUUID, "annotate", annotation.Description); err != nil {
if err := utils.ExecTaskInDir(tempDir, taskUUID, "annotate", annotation.Description); err != nil {
return fmt.Errorf("failed to add annotation %s: %v", annotation.Description, err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion backend/utils/tw/export_tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

// export the tasks so as to add them to DB
func ExportTasks(tempDir string) ([]models.Task, error) {
output, err := utils.ExecCommandForOutputInDir(tempDir, "task", "export")
output, err := utils.ExecTaskOutputInDir(tempDir, "export")
if err != nil {
return nil, fmt.Errorf("error executing Taskwarrior export command: %v", err)
}
Expand Down
7 changes: 2 additions & 5 deletions backend/utils/tw/fetch_tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@ import (
"os"
)

// complete logic (delete config if any->setup config->sync->get tasks->export)
func FetchTasksFromTaskwarrior(email, encryptionSecret, origin, UUID string) ([]models.Task, error) {
// temporary directory for each user
if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil {
return nil, fmt.Errorf("error deleting Taskwarrior data: %v", err)
}
taskwarriorMu.Lock()
defer taskwarriorMu.Unlock()

tempDir, err := os.MkdirTemp("", utils.SafeTempDirPrefix("taskwarrior-", email))
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions backend/utils/tw/isolation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package tw

import "sync"

// taskwarriorMu serializes all Taskwarrior CLI use so GET /tasks cannot
// overlap mutation jobs (and so concurrent HTTP fetches do not share a home).
var taskwarriorMu sync.Mutex
28 changes: 14 additions & 14 deletions backend/utils/tw/modify_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
)

func ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, email, encryptionSecret, taskID string, tags []string, depends []string) error {
if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil {
return fmt.Errorf("error deleting Taskwarrior data: %v", err)
}
taskwarriorMu.Lock()
defer taskwarriorMu.Unlock()

tempDir, err := os.MkdirTemp("", utils.SafeTempDirPrefix("taskwarrior-", email))
if err != nil {
return fmt.Errorf("failed to create temporary directory: %v", err)
Expand All @@ -28,41 +28,41 @@ func ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due,

escapedDescription := fmt.Sprintf(`description:"%s"`, strings.ReplaceAll(description, `"`, `\"`))

if err := utils.ExecCommand("task", taskID, "modify", escapedDescription); err != nil {
if err := utils.ExecTaskInDir(tempDir, taskID, "modify", escapedDescription); err != nil {
return fmt.Errorf("failed to edit task: %v", err)
}

escapedProject := fmt.Sprintf(`project:%s`, strings.ReplaceAll(project, `"`, `\"`))
if err := utils.ExecCommand("task", taskID, "modify", escapedProject); err != nil {
if err := utils.ExecTaskInDir(tempDir, taskID, "modify", escapedProject); err != nil {
return fmt.Errorf("failed to edit task project: %v", err)
}

escapedPriority := fmt.Sprintf(`priority:%s`, strings.ReplaceAll(priority, `"`, `\"`))
if err := utils.ExecCommand("task", taskID, "modify", escapedPriority); err != nil {
if err := utils.ExecTaskInDir(tempDir, taskID, "modify", escapedPriority); err != nil {
return fmt.Errorf("failed to edit task priority: %v", err)
}

escapedDue := fmt.Sprintf(`due:%s`, strings.ReplaceAll(due, `"`, `\"`))
if err := utils.ExecCommand("task", taskID, "modify", escapedDue); err != nil {
if err := utils.ExecTaskInDir(tempDir, taskID, "modify", escapedDue); err != nil {
return fmt.Errorf("failed to edit task due: %v", err)
}

// Handle dependencies - always set to ensure clearing works
if err := utils.ExecCommand("task", taskID, "modify", "depends:"); err != nil {
if err := utils.ExecTaskInDir(tempDir, taskID, "modify", "depends:"); err != nil {
return fmt.Errorf("failed to clear dependencies: %v", err)
}
if len(depends) > 0 {
dependsStr := strings.Join(depends, ",")
if err := utils.ExecCommand("task", taskID, "modify", "depends:"+dependsStr); err != nil {
if err := utils.ExecTaskInDir(tempDir, taskID, "modify", "depends:"+dependsStr); err != nil {
return fmt.Errorf("failed to set dependencies %s: %v", dependsStr, err)
}
}

// escapedStatus := fmt.Sprintf(`status:%s`, strings.ReplaceAll(status, `"`, `\"`))
if status == "completed" {
utils.ExecCommand("task", taskID, "done", "rc.confirmation=off")
utils.ExecTaskInDir(tempDir, taskID, "done", "rc.confirmation=off")
} else if status == "deleted" {
utils.ExecCommand("task", taskID, "delete", "rc.confirmation=off")
utils.ExecTaskInDir(tempDir, taskID, "delete", "rc.confirmation=off")
}

// Handle tags
Expand All @@ -71,18 +71,18 @@ func ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due,
if strings.HasPrefix(tag, "+") {
// Add tag
tagValue := strings.TrimPrefix(tag, "+")
if err := utils.ExecCommand("task", taskID, "modify", "+"+tagValue); err != nil {
if err := utils.ExecTaskInDir(tempDir, taskID, "modify", "+"+tagValue); err != nil {
return fmt.Errorf("failed to add tag %s: %v", tagValue, err)
}
} else if strings.HasPrefix(tag, "-") {
// Remove tag
tagValue := strings.TrimPrefix(tag, "-")
if err := utils.ExecCommand("task", taskID, "modify", "-"+tagValue); err != nil {
if err := utils.ExecTaskInDir(tempDir, taskID, "modify", "-"+tagValue); err != nil {
return fmt.Errorf("failed to remove tag %s: %v", tagValue, err)
}
} else {
// Add tag without prefix
if err := utils.ExecCommand("task", taskID, "modify", "+"+tag); err != nil {
if err := utils.ExecTaskInDir(tempDir, taskID, "modify", "+"+tag); err != nil {
return fmt.Errorf("failed to add tag %s: %v", tag, err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion backend/utils/tw/set_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func SetTaskwarriorConfig(tempDir, encryptionSecret, origin, UUID string) error
}

for _, args := range configCmds {
if err := utils.ExecCommandInDir(tempDir, args[0], args[1:]...); err != nil {
if err := utils.ExecTaskInDir(tempDir, args[1:]...); err != nil {
return fmt.Errorf("error setting Taskwarrior config (%v)", err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion backend/utils/tw/sync_tasks.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

// sync the user's tasks to all of their TW clients
func SyncTaskwarrior(tempDir string) error {
if err := utils.ExecCommandInDir(tempDir, "task", "sync"); err != nil {
if err := utils.ExecTaskInDir(tempDir, "sync"); err != nil {
return fmt.Errorf("error syncing Taskwarrior: %v", err)
}
return nil
Expand Down
Loading
Loading