Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/extension_bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ If applicable, add screenshots to help explain your problem.

**Environment Information:**
- Browser: [e.g. Chrome 134 / Firefox 135]
- Extension Version: [e.g. v0.1.0 see Support section in settings]
- Extension Version: [e.g. v0.1.0 - see Support section in settings]
- OS: [e.g. Windows 11 / macOS 14 / Ubuntu 24.04]
- Surge Core Version: [e.g. 1.2.0]

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ If Surge saves you time, consider supporting the development! Donations go direc

[**☕ Buy us a coffee**](https://www.buymeacoffee.com/surge.downloader)

_Totally optionalyour stars, issues, and contributions already mean the world to us! :)_
_Totally optional-your stars, issues, and contributions already mean the world to us! :)_

---

Expand Down Expand Up @@ -217,7 +217,7 @@ We tested Surge against standard tools. Because of our connection optimization l

| Tool | Time | Speed | Comparison |
| --------------- | ---------------- | -------------------- | ------------- |
| **Surge** | **28.93s** | **35.40 MB/s** | **** |
| **Surge** | **28.93s** | **35.40 MB/s** | **-** |
| aria2c | 40.04s | 25.57 MB/s | 1.38× slower |
| curl | 57.57s | 17.79 MB/s | 1.99× slower |
| wget | 61.81s | 16.57 MB/s | 2.14× slower |
Expand Down
Binary file modified assets/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions cmd/autoresume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ func TestCmd_AutoResume_Execution(t *testing.T) {
// 2. Settings with AutoResume = true
settingsPath := filepath.Join(surgeDir, "settings.json")
settings := config.DefaultSettings()
settings.General.AutoResume = true
settings.General.DefaultDownloadDir = tmpDir
settings.General.AutoResume.Value = true
settings.General.DefaultDownloadDir.Value = tmpDir

data, _ := json.Marshal(settings)
if err := os.WriteFile(settingsPath, data, 0o644); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ func TestConnectCmd_NoServerRunning(t *testing.T) {
t.Fatalf("Failed to ensure dirs: %v", err)
}

// No port file exists should return 0
// No port file exists - should return 0
port := readActivePort()
if port != 0 {
t.Fatalf("Expected port 0 (no server), got %d", port)
Expand Down
4 changes: 2 additions & 2 deletions cmd/connect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ func TestNewRemoteRootModel_UsesNilOrchestrator(t *testing.T) {
func TestNewRemoteRootModel_DownloadRequestUsesServiceAdd(t *testing.T) {
service := &fakeRemoteDownloadService{}
m := newRemoteRootModel("https://example.com:1700", service)
m.Settings.Extension.ExtensionPrompt = false
m.Settings.General.WarnOnDuplicate = false
m.Settings.Extension.ExtensionPrompt.Value = false
m.Settings.General.WarnOnDuplicate.Value = false

updated, cmd := m.Update(events.DownloadRequestMsg{
URL: "https://example.com/file.bin",
Expand Down
8 changes: 4 additions & 4 deletions cmd/headless_approval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestHandleDownload_HeadlessMode_AutoApprovesNonDuplicate(t *testing.T) {

// Enable ExtensionPrompt (default is true, but let's be explicit)
settings := config.DefaultSettings()
settings.Extension.ExtensionPrompt = true
settings.Extension.ExtensionPrompt.Value = true
if err := config.SaveSettings(settings); err != nil {
t.Fatalf("SaveSettings failed: %v", err)
}
Expand Down Expand Up @@ -94,7 +94,7 @@ func TestHandleDownload_HeadlessMode_RejectsDuplicateWithWarn(t *testing.T) {

// Enable WarnOnDuplicate
settings := config.DefaultSettings()
settings.General.WarnOnDuplicate = true
settings.General.WarnOnDuplicate.Value = true
if err := config.SaveSettings(settings); err != nil {
t.Fatalf("SaveSettings failed: %v", err)
}
Expand Down Expand Up @@ -145,8 +145,8 @@ func TestHandleDownload_HeadlessMode_RejectsExtensionPromptDuplicate(t *testing.
})

settings := config.DefaultSettings()
settings.Extension.ExtensionPrompt = true
settings.General.WarnOnDuplicate = false
settings.Extension.ExtensionPrompt.Value = true
settings.General.WarnOnDuplicate.Value = false
if err := config.SaveSettings(settings); err != nil {
t.Fatalf("SaveSettings failed: %v", err)
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/http_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"sort"
"strings"

"github.com/SurgeDM/Surge/internal/config"
"github.com/SurgeDM/Surge/internal/core"
"github.com/SurgeDM/Surge/internal/engine/events"
"github.com/SurgeDM/Surge/internal/utils"
Expand Down Expand Up @@ -291,7 +292,7 @@ func ensureOpenActionRequestAllowed(r *http.Request) error {
}

settings := getSettings()
if settings != nil && settings.General.AllowRemoteOpenActions {
if settings != nil && config.Resolve[bool](settings.General.AllowRemoteOpenActions) {
return nil
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/http_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func TestEnsureOpenActionRequestAllowed_RemoteToggle(t *testing.T) {
}

globalSettings = config.DefaultSettings()
globalSettings.General.AllowRemoteOpenActions = true
globalSettings.General.AllowRemoteOpenActions.Value = true
if err := ensureOpenActionRequestAllowed(request); err != nil {
t.Fatalf("expected remote open action to be allowed when enabled, got: %v", err)
}
Expand Down Expand Up @@ -380,7 +380,7 @@ func TestEnsureOpenActionRequestAllowed_ForwardedLoopbackDenied(t *testing.T) {
}

globalSettings = config.DefaultSettings()
globalSettings.General.AllowRemoteOpenActions = true
globalSettings.General.AllowRemoteOpenActions.Value = true
if err := ensureOpenActionRequestAllowed(request); err != nil {
t.Fatalf("expected forwarded loopback request to be allowed when enabled, got: %v", err)
}
Expand Down
8 changes: 4 additions & 4 deletions cmd/http_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ func TestHandleDownload_PathResolution(t *testing.T) {

// Create a temporary settings file
settings := config.DefaultSettings()
settings.General.DefaultDownloadDir = defaultDownloadDir
settings.Extension.ExtensionPrompt = false
settings.General.DefaultDownloadDir.Value = defaultDownloadDir
settings.Extension.ExtensionPrompt.Value = false

if err := config.SaveSettings(settings); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -442,8 +442,8 @@ func TestHandleDownload_PublishError_RecordsPreflightError(t *testing.T) {
t.Cleanup(func() { serverProgram = origServerProgram })

settings := config.DefaultSettings()
settings.Extension.ExtensionPrompt = true
settings.General.WarnOnDuplicate = false
settings.Extension.ExtensionPrompt.Value = true
settings.General.WarnOnDuplicate.Value = false
if err := config.SaveSettings(settings); err != nil {
t.Fatalf("SaveSettings failed: %v", err)
}
Expand Down
9 changes: 6 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ var (
globalEnqueueMu sync.Mutex
)

func buildPoolIsNameActive(getAll func() []types.DownloadConfig) processing.IsNameActiveFunc {
// buildActiveDownloadChecker bridges the lifecycle manager and the worker pool.
// LifecycleManager has no direct reference to the pool, so we inject this closure
// at construction time to let it detect file-name collisions with in-flight downloads.
func buildActiveDownloadChecker(getAll func() []types.DownloadConfig) processing.IsNameActiveFunc {
if getAll == nil {
return nil
}
Expand Down Expand Up @@ -138,7 +141,7 @@ func newLocalLifecycleManager(service core.DownloadService, getAll func() []type
addWithIDFunc = service.AddWithID
}

return processing.NewLifecycleManager(addFunc, addWithIDFunc, buildPoolIsNameActive(getAll))
return processing.NewLifecycleManager(addFunc, addWithIDFunc, buildActiveDownloadChecker(getAll))
}

func startLifecycleEventWorker(service core.DownloadService, mgr *processing.LifecycleManager) (func(), error) {
Expand Down Expand Up @@ -437,7 +440,7 @@ var rootCmd = &cobra.Command{
PersistentPreRun: func(cmd *cobra.Command, args []string) {
GlobalProgressCh = make(chan any, 100)
globalSettings = getSettings()
GlobalPool = download.NewWorkerPool(GlobalProgressCh, globalSettings.Network.MaxConcurrentDownloads)
GlobalPool = download.NewWorkerPool(GlobalProgressCh, config.Resolve[int](globalSettings.Network.MaxConcurrentDownloads))
},
RunE: func(cmd *cobra.Command, args []string) error {
if ranRemote, err := maybeRunRemoteTUI(cmd, args); err != nil {
Expand Down
18 changes: 9 additions & 9 deletions cmd/root_downloads.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func maybeRequireDownloadApproval(w http.ResponseWriter, service core.DownloadSe
return false
}

shouldPrompt := resolved.settings.Extension.ExtensionPrompt || (resolved.settings.General.WarnOnDuplicate && resolved.isDuplicate)
shouldPrompt := config.Resolve[bool](resolved.settings.Extension.ExtensionPrompt) || (config.Resolve[bool](resolved.settings.General.WarnOnDuplicate) && resolved.isDuplicate)
if !shouldPrompt {
return false
}
Expand Down Expand Up @@ -325,7 +325,7 @@ func processDownloads(urls []string, outputDir string, port int) int {
outPath = utils.EnsureAbsPath(outPath)

// CLI explicit arg means we do not auto-route when user provided an explicit output path.
isExplicit := isExplicitOutputPath(outPath, settings.General.DefaultDownloadDir)
isExplicit := isExplicitOutputPath(outPath, config.Resolve[string](settings.General.DefaultDownloadDir))
if lifecycle == nil {
err := fmt.Errorf("lifecycle manager unavailable")
recordPreflightDownloadError(url, outPath, err)
Expand Down Expand Up @@ -358,7 +358,7 @@ func resolveOutputDir(reqPath string, relativeToDefaultDir bool, defaultOutputDi
}

if relativeToDefaultDir && reqPath != "" {
baseDir := settings.General.DefaultDownloadDir
baseDir := config.Resolve[string](settings.General.DefaultDownloadDir)
if baseDir == "" {
baseDir = defaultOutputDir
}
Expand All @@ -369,8 +369,8 @@ func resolveOutputDir(reqPath string, relativeToDefaultDir bool, defaultOutputDi
} else if outPath == "" {
if defaultOutputDir != "" {
outPath = defaultOutputDir
} else if settings.General.DefaultDownloadDir != "" {
outPath = settings.General.DefaultDownloadDir
} else if config.Resolve[string](settings.General.DefaultDownloadDir) != "" {
outPath = config.Resolve[string](settings.General.DefaultDownloadDir)
} else {
outPath = "."
}
Expand All @@ -387,16 +387,16 @@ func mapClientWindowsPath(reqPath string, relativeToDefaultDir bool, defaultOutp

baseDir := "."
if relativeToDefaultDir {
if settings != nil && strings.TrimSpace(settings.General.DefaultDownloadDir) != "" {
baseDir = settings.General.DefaultDownloadDir
if settings != nil && strings.TrimSpace(config.Resolve[string](settings.General.DefaultDownloadDir)) != "" {
baseDir = config.Resolve[string](settings.General.DefaultDownloadDir)
} else if strings.TrimSpace(defaultOutputDir) != "" {
baseDir = defaultOutputDir
}
} else {
if strings.TrimSpace(defaultOutputDir) != "" {
baseDir = defaultOutputDir
} else if settings != nil && strings.TrimSpace(settings.General.DefaultDownloadDir) != "" {
baseDir = settings.General.DefaultDownloadDir
} else if settings != nil && strings.TrimSpace(config.Resolve[string](settings.General.DefaultDownloadDir)) != "" {
baseDir = config.Resolve[string](settings.General.DefaultDownloadDir)
}
}

Expand Down
16 changes: 8 additions & 8 deletions cmd/root_lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (s *countingLifecycleService) StreamEvents(context.Context) (<-chan interfa
return ch, cleanup, nil
}

func TestBuildPoolIsNameActive(t *testing.T) {
func TestBuildActiveDownloadChecker(t *testing.T) {
getAll := func() []types.DownloadConfig {
state := types.NewProgressState("dl-2", 0)
state.SetFilename("from-state.iso")
Expand All @@ -85,7 +85,7 @@ func TestBuildPoolIsNameActive(t *testing.T) {
}
}

isNameActive := buildPoolIsNameActive(getAll)
isNameActive := buildActiveDownloadChecker(getAll)
if isNameActive == nil {
t.Fatal("expected name activity callback")
}
Expand Down Expand Up @@ -239,8 +239,8 @@ func TestProcessDownloads_RoutesBinFilesToCustomCategory(t *testing.T) {
t.Fatalf("MkdirAll failed: %v", err)
}
settings := config.DefaultSettings()
settings.General.DefaultDownloadDir = defaultDir
settings.Categories.CategoryEnabled = true
settings.General.DefaultDownloadDir.Value = defaultDir
settings.Categories.CategoryEnabled.Value = true
settings.Categories.Categories = append(settings.Categories.Categories, config.Category{
Name: "Binary",
Pattern: `(?i)\.bin$`,
Expand Down Expand Up @@ -325,8 +325,8 @@ func TestProcessDownloads_UsesLatestSavedCategorySettings(t *testing.T) {

defaultDir := t.TempDir()
initial := config.DefaultSettings()
initial.General.DefaultDownloadDir = defaultDir
initial.Categories.CategoryEnabled = false
initial.General.DefaultDownloadDir.Value = defaultDir
initial.Categories.CategoryEnabled.Value = false
if err := config.SaveSettings(initial); err != nil {
t.Fatalf("SaveSettings(initial) failed: %v", err)
}
Expand All @@ -343,8 +343,8 @@ func TestProcessDownloads_UsesLatestSavedCategorySettings(t *testing.T) {
t.Fatalf("MkdirAll failed: %v", err)
}
updated := config.DefaultSettings()
updated.General.DefaultDownloadDir = defaultDir
updated.Categories.CategoryEnabled = true
updated.General.DefaultDownloadDir.Value = defaultDir
updated.Categories.CategoryEnabled.Value = true
updated.Categories.Categories = []config.Category{
{
Name: "Binary",
Expand Down
4 changes: 2 additions & 2 deletions cmd/root_startup.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func initializeGlobalState() error {
utils.ConfigureDebug(logsDir)

// Clean up old logs (keeping retention-1 because a new log will be created immediately after)
retention := getSettings().General.LogRetentionCount
retention := config.Resolve[int](getSettings().General.LogRetentionCount)
if retention > 0 {
utils.CleanupLogs(retention - 1)
} else {
Expand Down Expand Up @@ -83,7 +83,7 @@ func resumePausedDownloads() {
for _, entry := range pausedEntries {
// If entry is explicitly queued, we should start it regardless of AutoResume setting
// If entry is paused, we only start it if AutoResume is enabled
if entry.Status == "paused" && !settings.General.AutoResume {
if entry.Status == "paused" && !config.Resolve[bool](settings.General.AutoResume) {
continue
}
if GlobalService == nil || entry.ID == "" {
Expand Down
2 changes: 1 addition & 1 deletion cmd/service_ui_std.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func configureServiceUI(m *tui.RootModel) {
}
status, statusErr := s.Status()
if statusErr == nil {
m.Settings.General.AutoStart = (status == service.StatusRunning || status == service.StatusStopped)
m.Settings.General.AutoStart.Value = (status == service.StatusRunning || status == service.StatusStopped)
}

m.ToggleServiceFunc = func(enable bool) error {
Expand Down
2 changes: 1 addition & 1 deletion cmd/startup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func setupTestEnv(t *testing.T, tmpDir string) {

// Setup Settings (AutoResume=false default)
settings := config.DefaultSettings()
settings.General.AutoResume = false // Ensure we test that "queued" overrides this
settings.General.AutoResume.Value = false // Ensure we test that "queued" overrides this
if err := config.SaveSettings(settings); err != nil {
t.Fatal(err)
}
Expand Down
12 changes: 6 additions & 6 deletions extension/entrypoints/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ function updateBadge(): void {
}

async function tryOpenPopup(): Promise<void> {
try { await browser.action.openPopup(); } catch { /* ignore requires focused window */ }
try { await browser.action.openPopup(); } catch { /* ignore - requires focused window */ }
}

async function isDuplicateDownload(url: string): Promise<boolean> {
Expand All @@ -476,7 +476,7 @@ async function handleDownloadCreated(downloadItem: {
try {
await browser.downloads.cancel(downloadItem.id);
await browser.downloads.erase({ id: downloadItem.id } as any);
} catch { /* already completed or removed ignore */ }
} catch { /* already completed or removed - ignore */ }

const { filename, directory } = extractPathInfo(downloadItem);
const duplicateDisplayName = filename || downloadItem.url.split('/').pop()?.split('?')[0] || 'Unknown file';
Expand Down Expand Up @@ -549,7 +549,7 @@ async function startSSEStream(): Promise<void> {
return;
}

// Connected reset retry backoff
// Connected - reset retry backoff
sseRetryCount = 0;

const reader = resp.body.getReader();
Expand Down Expand Up @@ -789,7 +789,7 @@ export default defineBackground(() => {
}
});

// Header capture Firefox doesn't support the extraHeaders permission
// Header capture - Firefox doesn't support the extraHeaders permission
const isFF = (browser.runtime.getURL as (path?: string) => string)('').startsWith('moz-extension:');
const listenerOptions: Parameters<typeof browser.webRequest.onBeforeSendHeaders.addListener>[2] = ['requestHeaders'];
if (!isFF) listenerOptions.push('extraHeaders');
Expand All @@ -802,7 +802,7 @@ export default defineBackground(() => {
listenerOptions,
);

// Message handler Chrome MV3 requires sendResponse + return true for async responses.
// Message handler - Chrome MV3 requires sendResponse + return true for async responses.
// Returning a Promise from onMessage does NOT work without webextension-polyfill.
browser.runtime.onMessage.addListener(((
message: Record<string, any>,
Expand All @@ -818,7 +818,7 @@ export default defineBackground(() => {
return true;
}) as Parameters<typeof browser.runtime.onMessage.addListener>[0]);

// Health check start SSE stream when connection is established
// Health check - start SSE stream when connection is established
setInterval(async () => {
const wasConnected = isConnected;
await checkHealthSilent();
Expand Down
Loading
Loading