diff --git a/server/internal/server/server.go b/server/internal/server/server.go
index 7ba2b61..e9e1493 100644
--- a/server/internal/server/server.go
+++ b/server/internal/server/server.go
@@ -175,7 +175,7 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
)
}
- if err := t.Send(newFilename, convertedData); err != nil {
+ if err := t.Send(r.Context(), newFilename, convertedData); err != nil {
slog.Error("target failed", "target", t.Type(), "source", header.Filename, "file", newFilename, "error", err)
http.Error(w, "target forward failed", http.StatusBadGateway)
return
diff --git a/server/internal/server/server_test.go b/server/internal/server/server_test.go
index 4e99fd9..fb72d22 100644
--- a/server/internal/server/server_test.go
+++ b/server/internal/server/server_test.go
@@ -2,6 +2,7 @@ package server
import (
"bytes"
+ "context"
"database/sql"
"fmt"
"mime/multipart"
@@ -24,7 +25,7 @@ type mockTarget struct {
func (m *mockTarget) Type() string { return "mock" }
func (m *mockTarget) AcceptedFormats() []string { return []string{"gpx_1.1", "geojson"} }
-func (m *mockTarget) Send(filename string, data []byte) error { return m.err }
+func (m *mockTarget) Send(_ context.Context, filename string, data []byte) error { return m.err }
type countTarget struct {
count *int
@@ -32,7 +33,7 @@ type countTarget struct {
func (c *countTarget) Type() string { return "count" }
func (c *countTarget) AcceptedFormats() []string { return []string{"gpx_1.1", "geojson"} }
-func (c *countTarget) Send(filename string, data []byte) error {
+func (c *countTarget) Send(_ context.Context, filename string, data []byte) error {
*c.count++
return nil
}
diff --git a/server/internal/target/dawarich/dawarich.go b/server/internal/target/dawarich/dawarich.go
index 905e3a3..0ccf2f0 100644
--- a/server/internal/target/dawarich/dawarich.go
+++ b/server/internal/target/dawarich/dawarich.go
@@ -2,6 +2,7 @@ package dawarich
import (
"bytes"
+ "context"
"fmt"
"io"
"mime/multipart"
@@ -37,7 +38,7 @@ type Dawarich struct {
func (d *Dawarich) Type() string { return "dawarich" }
func (d *Dawarich) AcceptedFormats() []string { return []string{"geojson", "gpx_1.1"} }
-func (d *Dawarich) Send(filename string, data []byte) error {
+func (d *Dawarich) Send(ctx context.Context, filename string, data []byte) error {
apiKey, err := d.readAPIKey()
if err != nil {
return fmt.Errorf("reading API key: %w", err)
@@ -57,7 +58,7 @@ func (d *Dawarich) Send(filename string, data []byte) error {
}
url := d.cfg.URL + "/api/v1/imports"
- req, err := http.NewRequest("POST", url, &buf)
+ req, err := http.NewRequestWithContext(ctx, "POST", url, &buf)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
diff --git a/server/internal/target/dawarich/dawarich_test.go b/server/internal/target/dawarich/dawarich_test.go
index 826d702..3ffb83e 100644
--- a/server/internal/target/dawarich/dawarich_test.go
+++ b/server/internal/target/dawarich/dawarich_test.go
@@ -1,6 +1,7 @@
package dawarich
import (
+ "context"
"io"
"net/http"
"net/http/httptest"
@@ -31,7 +32,7 @@ func TestSend_Success(t *testing.T) {
client: &http.Client{Timeout: 5 * time.Second},
}
- require.NoError(t, d.Send("track.gpx", []byte("data")))
+ require.NoError(t, d.Send(context.Background(), "track.gpx", []byte("data")))
assert.Equal(t, "Bearer test-key", gotAuth)
assert.True(t, strings.HasPrefix(gotContentType, "multipart/form-data"))
assert.Contains(t, gotBody, "track.gpx")
@@ -50,7 +51,7 @@ func TestSend_PostsToImportsEndpoint(t *testing.T) {
cfg: target.Config{URL: ts.URL, APIKey: "k"},
client: &http.Client{Timeout: 5 * time.Second},
}
- _ = d.Send("f.gpx", []byte("data"))
+ _ = d.Send(context.Background(), "f.gpx", []byte("data"))
assert.Equal(t, "/api/v1/imports", gotPath)
}
@@ -66,7 +67,7 @@ func TestSend_ErrorOnNon2xx(t *testing.T) {
client: &http.Client{Timeout: 5 * time.Second},
}
- err := d.Send("f.gpx", []byte("data"))
+ err := d.Send(context.Background(), "f.gpx", []byte("data"))
require.Error(t, err)
assert.Contains(t, err.Error(), "500")
}
diff --git a/server/internal/target/target.go b/server/internal/target/target.go
index e38833a..2d6c104 100644
--- a/server/internal/target/target.go
+++ b/server/internal/target/target.go
@@ -1,6 +1,7 @@
package target
import (
+ "context"
"fmt"
"time"
)
@@ -13,7 +14,7 @@ type Target interface {
// ordered by preference (most preferred first).
AcceptedFormats() []string
// Send forwards a GPS track file to the target.
- Send(filename string, data []byte) error
+ Send(ctx context.Context, filename string, data []byte) error
}
// Constructor creates a new Target from the given config.
diff --git a/server/main.go b/server/main.go
index 4c72009..584b002 100644
--- a/server/main.go
+++ b/server/main.go
@@ -67,8 +67,10 @@ func main() {
addr := ":" + cfg.Port
httpServer := &http.Server{
- Addr: addr,
- Handler: srv.Handler(),
+ Addr: addr,
+ Handler: srv.Handler(),
+ ReadTimeout: 30 * time.Second,
+ WriteTimeout: cfg.TargetTimeout + 10*time.Second,
}
go func() {
diff --git a/tracksync/internal/sync/sync.go b/tracksync/internal/sync/sync.go
index 98e0cf7..2a24ea5 100644
--- a/tracksync/internal/sync/sync.go
+++ b/tracksync/internal/sync/sync.go
@@ -2,6 +2,7 @@ package sync
import (
"bytes"
+ "context"
"crypto/sha256"
"database/sql"
"encoding/hex"
@@ -81,7 +82,7 @@ const (
StatusDuplicate // server already had this file
)
-func Upload(client *http.Client, serverURL, token, deviceID, sourceFormat, filename string, data []byte) (UploadStatus, error) {
+func Upload(ctx context.Context, client *http.Client, serverURL, token, deviceID, sourceFormat, filename string, data []byte) (UploadStatus, error) {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
part, err := writer.CreateFormFile("file", filename)
@@ -95,7 +96,7 @@ func Upload(client *http.Client, serverURL, token, deviceID, sourceFormat, filen
return 0, fmt.Errorf("closing form: %w", err)
}
- req, err := http.NewRequest("POST", serverURL+"/upload", &buf)
+ req, err := http.NewRequestWithContext(ctx, "POST", serverURL+"/upload", &buf)
if err != nil {
return 0, fmt.Errorf("creating request: %w", err)
}
@@ -133,7 +134,7 @@ type Summary struct {
}
// SyncFiles syncs a list of found files to the server.
-func SyncFiles(db *sql.DB, client *http.Client, serverURL, token, deviceID string, files []device.FoundFile) Summary {
+func SyncFiles(ctx context.Context, db *sql.DB, client *http.Client, serverURL, token, deviceID string, files []device.FoundFile) Summary {
var summary Summary
for _, ff := range files {
@@ -160,7 +161,7 @@ func SyncFiles(db *sql.DB, client *http.Client, serverURL, token, deviceID strin
continue
}
- status, err := Upload(client, serverURL, token, deviceID, ff.Format, name, data)
+ status, err := Upload(ctx, client, serverURL, token, deviceID, ff.Format, name, data)
if err != nil {
slog.Error("upload failed", "file", name, "error", err)
summary.Errors++
diff --git a/tracksync/internal/sync/sync_test.go b/tracksync/internal/sync/sync_test.go
index ac46308..198e00a 100644
--- a/tracksync/internal/sync/sync_test.go
+++ b/tracksync/internal/sync/sync_test.go
@@ -1,6 +1,7 @@
package sync
import (
+ "context"
"database/sql"
"fmt"
"net/http"
@@ -107,7 +108,7 @@ func TestUpload_Created(t *testing.T) {
defer ts.Close()
client := &http.Client{Timeout: 5 * time.Second}
- status, err := Upload(client, ts.URL, "tok", "dev-1", "gpx_1.1", "track.gpx", []byte(""))
+ status, err := Upload(context.Background(), client, ts.URL, "tok", "dev-1", "gpx_1.1", "track.gpx", []byte(""))
require.NoError(t, err)
assert.Equal(t, StatusUploaded, status)
}
@@ -120,7 +121,7 @@ func TestUpload_Duplicate(t *testing.T) {
defer ts.Close()
client := &http.Client{Timeout: 5 * time.Second}
- status, err := Upload(client, ts.URL, "tok", "dev", "gpx_1.1", "f.gpx", []byte("data"))
+ status, err := Upload(context.Background(), client, ts.URL, "tok", "dev", "gpx_1.1", "f.gpx", []byte("data"))
require.NoError(t, err)
assert.Equal(t, StatusDuplicate, status)
}
@@ -133,7 +134,7 @@ func TestUpload_ServerError(t *testing.T) {
defer ts.Close()
client := &http.Client{Timeout: 5 * time.Second}
- _, err := Upload(client, ts.URL, "tok", "dev", "gpx_1.1", "f.gpx", []byte("data"))
+ _, err := Upload(context.Background(), client, ts.URL, "tok", "dev", "gpx_1.1", "f.gpx", []byte("data"))
assert.Error(t, err)
}
@@ -149,7 +150,7 @@ func TestSyncFiles_Uploaded(t *testing.T) {
require.NoError(t, os.WriteFile(filepath.Join(dir, "track.gpx"), []byte(""), 0644))
files := []device.FoundFile{{Path: filepath.Join(dir, "track.gpx"), Format: "gpx_1.1"}}
- summary := SyncFiles(db, &http.Client{Timeout: 5 * time.Second}, ts.URL, "tok", "dev", files)
+ summary := SyncFiles(context.Background(), db, &http.Client{Timeout: 5 * time.Second}, ts.URL, "tok", "dev", files)
assert.Equal(t, 1, summary.Uploaded)
assert.Equal(t, 0, summary.Duplicate)
@@ -170,7 +171,7 @@ func TestSyncFiles_Duplicate(t *testing.T) {
require.NoError(t, os.WriteFile(filepath.Join(dir, "track.gpx"), []byte(""), 0644))
files := []device.FoundFile{{Path: filepath.Join(dir, "track.gpx"), Format: "gpx_1.1"}}
- summary := SyncFiles(db, &http.Client{Timeout: 5 * time.Second}, ts.URL, "tok", "dev", files)
+ summary := SyncFiles(context.Background(), db, &http.Client{Timeout: 5 * time.Second}, ts.URL, "tok", "dev", files)
assert.Equal(t, 0, summary.Uploaded)
assert.Equal(t, 1, summary.Duplicate)
@@ -194,7 +195,7 @@ func TestSyncFiles_SkippedClientSide(t *testing.T) {
require.NoError(t, RecordUpload(db, SHA256Hex(data), "track.gpx", "dev"))
files := []device.FoundFile{{Path: filepath.Join(dir, "track.gpx"), Format: "gpx_1.1"}}
- summary := SyncFiles(db, &http.Client{Timeout: 5 * time.Second}, ts.URL, "tok", "dev", files)
+ summary := SyncFiles(context.Background(), db, &http.Client{Timeout: 5 * time.Second}, ts.URL, "tok", "dev", files)
assert.Equal(t, 0, summary.Uploaded)
assert.Equal(t, 0, summary.Duplicate)
@@ -214,7 +215,7 @@ func TestSyncFiles_UploadError(t *testing.T) {
require.NoError(t, os.WriteFile(filepath.Join(dir, "track.gpx"), []byte(""), 0644))
files := []device.FoundFile{{Path: filepath.Join(dir, "track.gpx"), Format: "gpx_1.1"}}
- summary := SyncFiles(db, &http.Client{Timeout: 5 * time.Second}, ts.URL, "tok", "dev", files)
+ summary := SyncFiles(context.Background(), db, &http.Client{Timeout: 5 * time.Second}, ts.URL, "tok", "dev", files)
assert.Equal(t, 0, summary.Uploaded)
assert.Equal(t, 0, summary.Duplicate)
@@ -251,7 +252,7 @@ func TestSyncFiles_MixedResults(t *testing.T) {
{Path: filepath.Join(dir, "dup.gpx"), Format: "gpx_1.1"},
{Path: filepath.Join(dir, "skip.gpx"), Format: "gpx_1.1"},
}
- summary := SyncFiles(db, &http.Client{Timeout: 5 * time.Second}, ts.URL, "tok", "dev", files)
+ summary := SyncFiles(context.Background(), db, &http.Client{Timeout: 5 * time.Second}, ts.URL, "tok", "dev", files)
assert.Equal(t, 1, summary.Uploaded)
assert.Equal(t, 1, summary.Duplicate)
diff --git a/tracksync/main.go b/tracksync/main.go
index ada842b..aada883 100644
--- a/tracksync/main.go
+++ b/tracksync/main.go
@@ -1,11 +1,13 @@
package main
import (
+ "context"
"encoding/json"
"flag"
"log/slog"
"net/http"
"os"
+ "os/signal"
"path/filepath"
"time"
@@ -122,6 +124,9 @@ func main() {
return
}
+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
+ defer stop()
+
httpClient := &http.Client{Timeout: *timeout}
slog.Info("starting sync",
"device", *deviceID,
@@ -129,7 +134,7 @@ func main() {
"files", len(files),
)
- summary := sync.SyncFiles(db, httpClient, *serverURL, token, *deviceID, files)
+ summary := sync.SyncFiles(ctx, db, httpClient, *serverURL, token, *deviceID, files)
slog.Info("sync complete",
"uploaded", summary.Uploaded,