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
15 changes: 15 additions & 0 deletions internal/database/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ func validateScheduleServerIDs(schedule types.Schedule) error {
if schedule.Options.UseLibrespeed && !hasServerIDs(schedule) {
return fmt.Errorf("%w: librespeed schedules require at least one server ID", ErrInvalidInput)
}
if schedule.Options.UseURLDownload {
if schedule.Options.DownloadURL == "" && !hasServerIDs(schedule) {
return fmt.Errorf("%w: url_download schedules require a download URL or server ID", ErrInvalidInput)
}
if schedule.Options.DownloadThreads != 0 &&
schedule.Options.DownloadThreads != 2 &&
schedule.Options.DownloadThreads != 4 &&
schedule.Options.DownloadThreads != 8 {
return fmt.Errorf("%w: download threads must be 2, 4, or 8", ErrInvalidInput)
}
if schedule.Options.DownloadTimeout != 0 &&
(schedule.Options.DownloadTimeout < 1 || schedule.Options.DownloadTimeout > 300) {
return fmt.Errorf("%w: download timeout must be between 1 and 300 seconds", ErrInvalidInput)
}
}
return nil
}

Expand Down
9 changes: 9 additions & 0 deletions internal/server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ func (s *Server) handleSpeedTest(c *gin.Context) {
if opts.UseLibrespeed {
timeout = time.Duration(s.config.SpeedTest.Librespeed.Timeout) * time.Second
}
// For URL download, use user-specified timeout if provided
if opts.UseURLDownload {
if opts.DownloadTimeout > 0 {
timeout = time.Duration(opts.DownloadTimeout) * time.Second
} else {
// Default to 30 seconds if not specified
timeout = 30 * time.Second
}
}

// Use configured timeout
ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
Expand Down
19 changes: 19 additions & 0 deletions internal/speedtest/result_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package speedtest
import (
"context"
"fmt"
"net/url"
"time"

"github.com/rs/zerolog/log"
Expand Down Expand Up @@ -47,6 +48,24 @@ func (h *DefaultResultHandler) SaveResult(ctx context.Context, result *Result, t
case "librespeed":
serverHost = &result.Server
serverID = fmt.Sprintf("librespeed-%s", result.Server)
case "url_download":
if opts.DownloadURL != "" {
// Custom URL
u, err := url.Parse(opts.DownloadURL)
if err == nil && u.Host != "" {
host := u.Host
serverHost = &host
serverID = fmt.Sprintf("url-custom-%s", u.Host)
} else {
serverID = "url-custom-unknown"
}
} else if len(opts.ServerIDs) > 0 {
// Built-in IDC
serverID = opts.ServerIDs[0]
serverHost = &opts.ServerHost
} else {
serverID = "url-unknown"
}
case "speedtest":
// For speedtest.net, we'll need to extract host info from the result
serverID = result.Server
Expand Down
28 changes: 28 additions & 0 deletions internal/speedtest/speedtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package speedtest
import (
"context"
"fmt"
"time"

"github.com/rs/zerolog/log"

Expand Down Expand Up @@ -38,6 +39,7 @@ type service struct {
speedtestNetRunner *SpeedtestNetRunner
iperfRunner *IperfRunner
librespeedRunner *LibrespeedRunner
urlDownloadRunner *UrlDownloadRunner
resultHandler ResultHandler
}

Expand All @@ -54,6 +56,7 @@ func New(db database.Service, cfg config.SpeedTestConfig, notifier *notification
svc.speedtestNetRunner = NewSpeedtestNetRunner(cfg)
svc.iperfRunner = NewIperfRunner(cfg.IPerf)
svc.librespeedRunner = NewLibrespeedRunner(cfg.Librespeed)
svc.urlDownloadRunner = NewUrlDownloadRunner()

// Initialize GeoIP databases for all speedtest features (traceroute, MTR, etc.)
svc.initGeoIP()
Expand Down Expand Up @@ -103,10 +106,33 @@ func (s *service) RunTest(ctx context.Context, opts *types.TestOptions) (*Result
Bool("isScheduled", opts.IsScheduled).
Bool("useIperf", opts.UseIperf).
Bool("useLibrespeed", opts.UseLibrespeed).
Bool("useUrlDownload", opts.UseURLDownload).
Str("server_ids", fmt.Sprintf("%v", opts.ServerIDs)).
Str("server_host", opts.ServerHost).
Msg("Starting speed test coordination")

if opts.UseURLDownload {
log.Info().Msg("Using url_download runner")
s.urlDownloadRunner.SetProgressCallback(s.broadcastUpdate)
result, err := s.urlDownloadRunner.RunTest(ctx, opts)
if err != nil {
return nil, fmt.Errorf("url_download test failed: %w", err)
}

// Save the result (even if partial due to timeout, result is non-nil)
// Use independent context for save to avoid "context deadline exceeded" errors
// when the test itself times out but we still want to save partial results
if result != nil {
saveCtx, saveCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer saveCancel()
if err := s.resultHandler.SaveResult(saveCtx, result, "url_download", opts); err != nil {
log.Error().Err(err).Msg("Failed to save url_download result")
}
}

return result, nil
}

if opts.UseLibrespeed {
log.Info().Msg("Using librespeed runner (handles ping natively)")
return s.RunLibrespeedTest(ctx, opts)
Expand Down Expand Up @@ -169,6 +195,8 @@ func (s *service) GetServers(testType string) ([]ServerResponse, error) {
return s.GetLibrespeedServers()
case "iperf3":
return s.iperfRunner.GetServers()
case "url_download":
return s.urlDownloadRunner.GetServers()
case "speedtest":
return s.speedtestNetRunner.GetServers()
default:
Expand Down
Loading
Loading