From 280fff21a3f7074b4bd718e2ce0da00c1c2234fb Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Tue, 27 Jan 2026 17:01:21 +0100 Subject: [PATCH] feat: add GetTaskFile and RemoveAllTaskFiles methods Complete the Task File API implementation with: - GetTaskFile: retrieve single file metadata by ID - RemoveAllTaskFiles: remove all files attached to a task - Add Username/UserName fields to TaskFile struct - Add TaskScope convenience methods: GetFile, RemoveFile, DownloadFile, RemoveAllFiles - Comprehensive tests for all new methods Closes: kanboard-amh --- .beads/export-state/fbbdf412d0fd5173.json | 4 +- .beads/issues.jsonl | 1 + files.go | 32 +++ files_test.go | 240 ++++++++++++++++++++++ task_scope.go | 20 ++ types.go | 2 + 6 files changed, 297 insertions(+), 2 deletions(-) diff --git a/.beads/export-state/fbbdf412d0fd5173.json b/.beads/export-state/fbbdf412d0fd5173.json index 80ee16a..dc11523 100644 --- a/.beads/export-state/fbbdf412d0fd5173.json +++ b/.beads/export-state/fbbdf412d0fd5173.json @@ -1,6 +1,6 @@ { "worktree_root": "/home/oli/Dev/kanboard-api", - "last_export_commit": "449cd2626c2990199ccad4b08041c0e438858272", - "last_export_time": "2026-01-27T12:13:18.925035335+01:00", + "last_export_commit": "6f2f0a10fc517c5bd882dcd188e15b47affeaca1", + "last_export_time": "2026-01-27T12:13:35.241832799+01:00", "jsonl_hash": "a60a9c8ba481e1eb8a3879ae02ed3fadda20082c4f0a618fe05f2c1c58b02e43" } \ No newline at end of file diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index e0bb016..5f20bf0 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -3,6 +3,7 @@ {"id":"kanboard-7es","title":"JSON-RPC Request-ID: Zufälligen Wert statt fester 1 verwenden","description":"## Kontext\n\nIn jedem JSON-RPC Request an die Kanboard-API wird im Root-Objekt ein Feld `id` mitgeliefert. Dieses dient dazu, bei asynchroner Kommunikation Request und Response einander zuordnen zu können – die API liefert diese ID in der Antwort zurück.\n\n**Aktuell:** Die ID ist fest auf `1` gesetzt.\n\n## Anforderung\n\n1. **Wenn keine ID von außen gesetzt wird:** Die Library soll intern einen zufälligen Wert generieren\n2. **API-Dokumentation prüfen:** Welche Werte sind erlaubt? Welche Größenordnung? (vermutlich Integer)\n3. **Signatur beibehalten:** Die öffentliche API der Library-Funktionen soll unverändert bleiben\n4. **Interne Generierung:** Die Library bestimmt selbst einen zufälligen Wert\n\n## Implementierungshinweise\n\n- Prüfen: Kanboard JSON-RPC Dokumentation bezüglich erlaubter ID-Werte\n- Vermutlich: `int64` oder `int32` Range\n- Zufallsgenerator: `math/rand` mit Seed oder `crypto/rand` für bessere Verteilung\n- Ggf. bestehende `requestIDCounter` in `jsonrpc.go` (Zeile 40) anpassen oder ersetzen\n\n## Beispiel\n\n**Vorher (immer gleich):**\n```json\n{\"jsonrpc\": \"2.0\", \"method\": \"getTask\", \"id\": 1, \"params\": {...}}\n```\n\n**Nachher (zufällig):**\n```json\n{\"jsonrpc\": \"2.0\", \"method\": \"getTask\", \"id\": 847291536, \"params\": {...}}\n```\n\n## Referenz\n\n- Datei: `jsonrpc.go`\n- Zeile 17: `ID int64 \\`json:\"id\"\\``\n- Zeile 40: `requestIDCounter` (existiert bereits)","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-23T17:44:51.566737509+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T10:21:06.415129879+01:00","closed_at":"2026-01-27T10:21:06.415129879+01:00","close_reason":"Closed"} {"id":"kanboard-9wa","title":"Support custom authentication header name","description":"## Description\n\nKanboard supports using an alternative HTTP header for authentication when the server has specific configuration requirements.\n\nCurrently, authentication uses the standard `Authorization` header via Go's `SetBasicAuth()`. This needs to be configurable so users can specify a custom header name (e.g., `X-API-Auth`).\n\n## Requirements\n\n- Add an optional configuration parameter for custom auth header name\n- Default to standard `Authorization` header if not specified\n- When custom header is set, use that header name instead of `Authorization`\n- The header value format should remain the same (Basic Auth base64-encoded credentials)\n\n## Acceptance Criteria\n\n- [ ] New client configuration method (e.g., `WithAuthHeader(headerName string)`)\n- [ ] Default behavior unchanged when no custom header specified\n- [ ] Custom header name is used when configured\n- [ ] Works with both API token and basic auth\n- [ ] Tests cover default and custom header scenarios\n\n## Reference\n\nKanboard API documentation: \"You can use an alternative HTTP header for authentication if your server has a very specific configuration.\"","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-23T18:08:31.507616093+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-23T18:26:50.40804952+01:00","closed_at":"2026-01-23T18:26:50.40804952+01:00","close_reason":"Closed"} {"id":"kanboard-a7x","title":"Handle URLs already ending in /jsonrpc.php","description":"## Context\n\nThe `NewClient()` function in `client.go` always appends `/jsonrpc.php` to the base URL. This causes issues when users pass a URL that already includes the endpoint path.\n\n## Problem\n\nIf a user calls:\n```go\nclient := kanboard.NewClient(\"https://example.com/jsonrpc.php\")\n```\n\nThe resulting endpoint becomes `https://example.com/jsonrpc.php/jsonrpc.php`, which fails.\n\n## Solution\n\nModify `NewClient()` to detect and handle URLs that already end in `/jsonrpc.php`:\n\n```go\nfunc NewClient(baseURL string) *Client {\n // Ensure no trailing slash\n baseURL = strings.TrimSuffix(baseURL, \"/\")\n\n // Handle URLs that already include /jsonrpc.php\n endpoint := baseURL\n if !strings.HasSuffix(baseURL, \"/jsonrpc.php\") {\n endpoint = baseURL + \"/jsonrpc.php\"\n }\n\n c := \u0026Client{\n baseURL: baseURL,\n endpoint: endpoint,\n }\n // ... rest unchanged\n}\n```\n\n## Files to Modify\n\n- `client.go` - Update `NewClient()` to check for existing `/jsonrpc.php` suffix\n\n## Acceptance Criteria\n\n- [ ] `NewClient(\"https://example.com\")` → endpoint `https://example.com/jsonrpc.php`\n- [ ] `NewClient(\"https://example.com/\")` → endpoint `https://example.com/jsonrpc.php`\n- [ ] `NewClient(\"https://example.com/jsonrpc.php\")` → endpoint `https://example.com/jsonrpc.php`\n- [ ] `NewClient(\"https://example.com/kanboard/jsonrpc.php\")` → endpoint `https://example.com/kanboard/jsonrpc.php`\n- [ ] Tests written and passing","status":"closed","priority":2,"issue_type":"bug","owner":"mail@oliverjakoubek.de","created_at":"2026-01-27T10:25:51.077352962+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T10:27:15.189568683+01:00","closed_at":"2026-01-27T10:27:15.189568683+01:00","close_reason":"Closed"} +{"id":"kanboard-amh","title":"Implement Attachment API methods","description":"Add support for Kanboard's task file/attachment API methods.\n\n## API Methods to Implement\n\nBased on [Kanboard Task File Procedures](https://docs.kanboard.org/v1/api/task_file_procedures/):\n\n### 1. createTaskFile\n- **Purpose:** Create and upload a new task attachment\n- **Parameters:** `project_id` (int), `task_id` (int), `filename` (string), `blob` (base64-encoded content)\n- **Returns:** `file_id` on success, `false` on failure\n- **Note:** Limited by PHP config; unsuitable for large files\n\n### 2. getAllTaskFiles\n- **Purpose:** Get all files attached to a task\n- **Parameters:** `task_id` (int)\n- **Returns:** Array of file objects with properties: id, name, path, is_image, task_id, date, user_id, size, username, user_name\n\n### 3. getTaskFile\n- **Purpose:** Get single file metadata\n- **Parameters:** `file_id` (int)\n- **Returns:** File object (id, name, path, is_image, task_id, date, user_id, size)\n\n### 4. downloadTaskFile\n- **Purpose:** Download file contents\n- **Parameters:** `file_id` (int)\n- **Returns:** Base64-encoded string\n\n### 5. removeTaskFile\n- **Purpose:** Delete a file attachment\n- **Parameters:** `file_id` (int)\n- **Returns:** `true` on success, `false` on failure\n\n### 6. removeAllTaskFiles (bonus)\n- **Purpose:** Remove all files from a task\n- **Parameters:** `task_id` (int)\n- **Returns:** `true` on success, `false` on failure\n\n## Acceptance Criteria\n\n- [ ] Implement `CreateTaskFile` method with base64 blob upload\n- [ ] Implement `GetAllTaskFiles` method returning typed file slice\n- [ ] Implement `GetTaskFile` method returning file metadata struct\n- [ ] Implement `DownloadTaskFile` method returning decoded bytes\n- [ ] Implement `RemoveTaskFile` method\n- [ ] Implement `RemoveAllTaskFiles` method\n- [ ] Define `TaskFile` struct with all documented fields\n- [ ] Add comprehensive tests for all methods\n- [ ] Follow existing code patterns in the repository","status":"closed","priority":2,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-27T16:06:53.739050479+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-27T17:01:10.618378834+01:00","closed_at":"2026-01-27T17:01:10.618378834+01:00","close_reason":"Closed"} {"id":"kanboard-api-0fz","title":"Implement Category API methods","description":"Implement direct API methods for category operations.\n\n## Methods to implement\n- GetAllCategories(ctx, projectID int) ([]Category, error) - getAllCategories\n- GetCategory(ctx, categoryID int) (*Category, error) - getCategory (Nice-to-have)\n\n## Files to create\n- categories.go\n\n## Acceptance criteria\n- Proper error handling\n- Returns empty slice when no categories exist","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:35:16.6133153+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:25:07.250066801+01:00","closed_at":"2026-01-15T18:25:07.250066801+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-0fz","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:42:53.161416595+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-0fz","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:42:53.226963473+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-16r","title":"Implement Tag API methods (CRITICAL)","description":"Implement direct API methods for tag operations. Tags are CRITICAL - heavily used.\n\n## Direct Client methods (Must-have)\n- GetTaskTags(ctx, taskID int) (map[int]string, error) - getTaskTags\n- SetTaskTags(ctx, projectID, taskID int, tags []string) error - setTaskTags\n\n## Direct Client methods (Nice-to-have)\n- GetAllTags(ctx) ([]Tag, error) - getAllTags\n- GetTagsByProject(ctx, projectID int) ([]Tag, error) - getTagsByProject\n- CreateTag(ctx, projectID int, name, colorID string) (int, error) - createTag\n- UpdateTag(ctx, tagID int, name, colorID string) error - updateTag\n- RemoveTag(ctx, tagID int) error - removeTag\n\n## Files to create\n- tags.go\n\n## IMPORTANT NOTE\nsetTaskTags REPLACES ALL tags. Individual add/remove requires read-modify-write pattern (implemented in TaskScope).\n\n## Acceptance criteria\n- GetTaskTags returns map[tagID]tagName\n- SetTaskTags accepts tag names (auto-creates if needed)","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:36:08.526810135+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:20:17.392248254+01:00","closed_at":"2026-01-15T18:20:17.392248254+01:00","close_reason":"Closed","dependencies":[{"issue_id":"kanboard-api-16r","depends_on_id":"kanboard-api-uls","type":"blocks","created_at":"2026-01-15T17:43:49.223137796+01:00","created_by":"Oliver Jakoubek"},{"issue_id":"kanboard-api-16r","depends_on_id":"kanboard-api-cyc","type":"blocks","created_at":"2026-01-15T17:43:49.402237867+01:00","created_by":"Oliver Jakoubek"}]} {"id":"kanboard-api-25y","title":"Implement Timestamp type with JSON handling","description":"Implement custom Timestamp type that handles Kanboard's Unix timestamp format.\n\n## Requirements\n- Timestamp struct wrapping time.Time\n- UnmarshalJSON supporting:\n - Unix timestamps as integers\n - Empty strings and \"0\" as zero time\n - Zero value (0) as zero time\n- MarshalJSON returning Unix timestamp or 0 for zero time\n\n## Files to create\n- timestamp.go\n\n## Acceptance criteria\n- Correctly parses integer Unix timestamps\n- Handles empty strings and \"0\" strings\n- Zero time marshals to 0\n- Non-zero time marshals to Unix timestamp","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-15T17:34:55.0044989+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-15T18:15:27.299644047+01:00","closed_at":"2026-01-15T18:15:27.299644047+01:00","close_reason":"Closed"} diff --git a/files.go b/files.go index 5ed3d8f..e419afc 100644 --- a/files.go +++ b/files.go @@ -75,3 +75,35 @@ func (c *Client) RemoveTaskFile(ctx context.Context, fileID int) error { return nil } + +// GetTaskFile returns a single file's metadata by its ID. +func (c *Client) GetTaskFile(ctx context.Context, fileID int) (*TaskFile, error) { + params := map[string]int{"file_id": fileID} + + var result *TaskFile + if err := c.call(ctx, "getTaskFile", params, &result); err != nil { + return nil, fmt.Errorf("getTaskFile: %w", err) + } + + if result == nil { + return nil, fmt.Errorf("%w: file %d", ErrNotFound, fileID) + } + + return result, nil +} + +// RemoveAllTaskFiles removes all files attached to a task. +func (c *Client) RemoveAllTaskFiles(ctx context.Context, taskID int) error { + params := map[string]int{"task_id": taskID} + + var success bool + if err := c.call(ctx, "removeAllTaskFiles", params, &success); err != nil { + return fmt.Errorf("removeAllTaskFiles: %w", err) + } + + if !success { + return fmt.Errorf("removeAllTaskFiles: delete failed") + } + + return nil +} diff --git a/files_test.go b/files_test.go index 027c655..7d6b285 100644 --- a/files_test.go +++ b/files_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "net/http" "net/http/httptest" "testing" @@ -322,3 +323,242 @@ func TestTaskScope_UploadFile(t *testing.T) { t.Errorf("expected fileID=10, got %d", fileID) } } + +func TestClient_GetTaskFile(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req JSONRPCRequest + json.NewDecoder(r.Body).Decode(&req) + + if req.Method != "getTaskFile" { + t.Errorf("expected method=getTaskFile, got %s", req.Method) + } + + params := req.Params.(map[string]any) + if params["file_id"].(float64) != 5 { + t.Errorf("expected file_id=5, got %v", params["file_id"]) + } + + resp := JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: json.RawMessage(`{"id": "5", "name": "document.pdf", "path": "files/5", "is_image": "0", "task_id": "42", "user_id": "1", "size": "1024"}`), + } + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := NewClient(server.URL).WithAPIToken("test-token") + + file, err := client.GetTaskFile(context.Background(), 5) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if int(file.ID) != 5 { + t.Errorf("expected id=5, got %d", file.ID) + } + if file.Name != "document.pdf" { + t.Errorf("expected name='document.pdf', got %s", file.Name) + } + if int(file.TaskID) != 42 { + t.Errorf("expected task_id=42, got %d", file.TaskID) + } +} + +func TestClient_GetTaskFile_NotFound(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req JSONRPCRequest + json.NewDecoder(r.Body).Decode(&req) + + resp := JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: json.RawMessage(`null`), + } + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := NewClient(server.URL).WithAPIToken("test-token") + + _, err := client.GetTaskFile(context.Background(), 999) + if err == nil { + t.Fatal("expected error for not found file") + } + if !errors.Is(err, ErrNotFound) { + t.Errorf("expected ErrNotFound, got %v", err) + } +} + +func TestClient_RemoveAllTaskFiles(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req JSONRPCRequest + json.NewDecoder(r.Body).Decode(&req) + + if req.Method != "removeAllTaskFiles" { + t.Errorf("expected method=removeAllTaskFiles, got %s", req.Method) + } + + params := req.Params.(map[string]any) + if params["task_id"].(float64) != 42 { + t.Errorf("expected task_id=42, got %v", params["task_id"]) + } + + resp := JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: json.RawMessage(`true`), + } + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := NewClient(server.URL).WithAPIToken("test-token") + + err := client.RemoveAllTaskFiles(context.Background(), 42) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestClient_RemoveAllTaskFiles_Failure(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req JSONRPCRequest + json.NewDecoder(r.Body).Decode(&req) + + resp := JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: json.RawMessage(`false`), + } + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := NewClient(server.URL).WithAPIToken("test-token") + + err := client.RemoveAllTaskFiles(context.Background(), 42) + if err == nil { + t.Fatal("expected error for failed delete") + } +} + +func TestTaskScope_GetFile(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req JSONRPCRequest + json.NewDecoder(r.Body).Decode(&req) + + if req.Method != "getTaskFile" { + t.Errorf("expected method=getTaskFile, got %s", req.Method) + } + + resp := JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: json.RawMessage(`{"id": "5", "name": "file.txt", "task_id": "42", "is_image": "0"}`), + } + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := NewClient(server.URL).WithAPIToken("test-token") + + file, err := client.Task(42).GetFile(context.Background(), 5) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if int(file.ID) != 5 { + t.Errorf("expected id=5, got %d", file.ID) + } +} + +func TestTaskScope_DownloadFile(t *testing.T) { + testContent := []byte("file content") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req JSONRPCRequest + json.NewDecoder(r.Body).Decode(&req) + + if req.Method != "downloadTaskFile" { + t.Errorf("expected method=downloadTaskFile, got %s", req.Method) + } + + encoded := base64.StdEncoding.EncodeToString(testContent) + resp := JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: json.RawMessage(`"` + encoded + `"`), + } + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := NewClient(server.URL).WithAPIToken("test-token") + + content, err := client.Task(42).DownloadFile(context.Background(), 5) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if string(content) != string(testContent) { + t.Errorf("expected content='%s', got '%s'", testContent, content) + } +} + +func TestTaskScope_RemoveFile(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req JSONRPCRequest + json.NewDecoder(r.Body).Decode(&req) + + if req.Method != "removeTaskFile" { + t.Errorf("expected method=removeTaskFile, got %s", req.Method) + } + + resp := JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: json.RawMessage(`true`), + } + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := NewClient(server.URL).WithAPIToken("test-token") + + err := client.Task(42).RemoveFile(context.Background(), 5) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestTaskScope_RemoveAllFiles(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req JSONRPCRequest + json.NewDecoder(r.Body).Decode(&req) + + if req.Method != "removeAllTaskFiles" { + t.Errorf("expected method=removeAllTaskFiles, got %s", req.Method) + } + + params := req.Params.(map[string]any) + if params["task_id"].(float64) != 42 { + t.Errorf("expected task_id=42, got %v", params["task_id"]) + } + + resp := JSONRPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: json.RawMessage(`true`), + } + json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := NewClient(server.URL).WithAPIToken("test-token") + + err := client.Task(42).RemoveAllFiles(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/task_scope.go b/task_scope.go index 446c8db..32e150a 100644 --- a/task_scope.go +++ b/task_scope.go @@ -281,3 +281,23 @@ func (t *TaskScope) UploadFile(ctx context.Context, filename string, content []b } return t.client.CreateTaskFile(ctx, int(task.ProjectID), t.taskID, filename, content) } + +// GetFile returns a file's metadata by ID. +func (t *TaskScope) GetFile(ctx context.Context, fileID int) (*TaskFile, error) { + return t.client.GetTaskFile(ctx, fileID) +} + +// RemoveFile removes a file by ID. +func (t *TaskScope) RemoveFile(ctx context.Context, fileID int) error { + return t.client.RemoveTaskFile(ctx, fileID) +} + +// DownloadFile downloads a file's content by ID. +func (t *TaskScope) DownloadFile(ctx context.Context, fileID int) ([]byte, error) { + return t.client.DownloadTaskFile(ctx, fileID) +} + +// RemoveAllFiles removes all files from this task. +func (t *TaskScope) RemoveAllFiles(ctx context.Context) error { + return t.client.RemoveAllTaskFiles(ctx, t.taskID) +} diff --git a/types.go b/types.go index c729dae..7c330f0 100644 --- a/types.go +++ b/types.go @@ -237,6 +237,8 @@ type TaskFile struct { DateCreation Timestamp `json:"date_creation"` UserID StringInt `json:"user_id"` Size StringInt64 `json:"size"` + Username string `json:"username"` // Only returned by getAllTaskFiles + UserName string `json:"user_name"` // Only returned by getAllTaskFiles } // Tag represents a Kanboard tag.