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,