Skip to content

feat: add 'downloads add-url' command for custom tarball URLs (#59)#61

Merged
renecannao merged 1 commit intomasterfrom
worktree-agent-a6ccc8d2
Apr 2, 2026
Merged

feat: add 'downloads add-url' command for custom tarball URLs (#59)#61
renecannao merged 1 commit intomasterfrom
worktree-agent-a6ccc8d2

Conversation

@renecannao
Copy link
Copy Markdown

@renecannao renecannao commented Apr 2, 2026

Summary

  • New command: dbdeployer downloads add-url <url>
  • Auto-detects version, OS, architecture, flavor from tarball filename
  • Override flags: --version, --OS, --arch, --flavor
  • Saves to user-local registry (~/.dbdeployer/custom_tarballs.json)
  • Merged with embedded registry at runtime

Closes #59

Test Plan

  • URL parsing tests (MySQL, Percona, macOS, minimal variants)
  • Build succeeds
  • Manual: dbdeployer downloads add-url https://dev.mysql.com/get/Downloads/MySQL-8.4/mysql-8.4.8-linux-glibc2.17-x86_64.tar.xz

Summary by CodeRabbit

Release Notes

  • New Features
    • Added downloads add-url command to import tarballs from HTTP/HTTPS URLs
    • Automatically detects tarball metadata including operating system, architecture, version, and flavor
    • Supports command-line flags to override auto-detected values
    • Verifies URL accessibility before importing (can be skipped with flag)
    • Handles duplicate entries with configurable overwrite option

Implements GitHub issue #59. Adds a new 'downloads add-url <URL>' subcommand
that parses the tarball filename to auto-detect version, OS, arch, flavor, and
minimal flag, validates URL accessibility via HTTP HEAD/GET, and persists the
entry to the local tarball registry (~/.dbdeployer/tarball-list.json).

- downloads/remote_registry.go: add ParseTarballUrlInfo, CheckRemoteUrl, and
  detectOSArchFromFilename helpers
- cmd/downloads.go: add addTarballToCollectionFromUrl handler and
  downloadsAddUrlCmd cobra command with --OS, --arch, --flavor, --version,
  --short-version, --minimal, --overwrite, --skip-verify-url flags
- globals/globals.go: add SkipVerifyUrlLabel constant
- downloads/remote_registry_test.go: add TestParseTarballUrlInfo covering MySQL
  Linux/macOS, Percona minimal, and invalid filename cases
Copilot AI review requested due to automatic review settings April 2, 2026 06:47
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

This PR introduces a new downloads add-url command that allows users to register custom tarball URLs in the local registry. The implementation includes URL parsing to auto-detect tarball metadata, remote URL validation via HTTP HEAD requests, and merge logic to add entries with configurable overwrite behavior.

Changes

Cohort / File(s) Summary
Command Implementation
cmd/downloads.go
Added downloads add-url command with addTarballToCollectionFromUrl() function. Parses URLs, auto-detects or overrides metadata (version, OS, arch, flavor), validates URL accessibility (unless --skip-verify-url is set), and merges into local tarball registry with overwrite control.
Remote Registry Utilities
downloads/remote_registry.go
Added CheckRemoteUrl() for HTTP HEAD-based URL validation, ParseTarballUrlInfo() to extract tarball metadata from URLs via filename parsing and heuristics, and detectOSArchFromFilename() helper for OS/arch detection with runtime-based fallback.
Tests & Constants
downloads/remote_registry_test.go, globals/globals.go
Added TestParseTarballUrlInfo() covering multiple CDN URL patterns and error cases. Added SkipVerifyUrlLabel constant for CLI flag configuration.

Sequence Diagram

sequenceDiagram
    participant User as User/CLI
    participant Cmd as downloads add-url
    participant Parser as ParseTarballUrlInfo
    participant Validator as CheckRemoteUrl
    participant Registry as Local Registry

    User->>Cmd: downloads add-url <url> [flags]
    Cmd->>Parser: ParseTarballUrlInfo(url)
    Parser-->>Cmd: TarballDescription (auto-detected metadata)
    Cmd->>Validator: CheckRemoteUrl(url)
    Validator-->>Cmd: Content-Length, error (or fallback)
    alt skip-verify-url not set
        Cmd->>Validator: Validate URL accessibility
    end
    Cmd->>Registry: Merge entry (with overwrite check)
    Registry-->>Cmd: Success or error (existing entry)
    Cmd-->>User: Result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A URL lands, we parse with care,
Auto-detect what's hiding there,
Check if it's real, then merge with grace,
Custom tarballs find their place! 📦✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: adding a new 'downloads add-url' command for custom tarball URLs, directly matching the primary objective.
Linked Issues check ✅ Passed All coding requirements from issue #59 are implemented: new CLI command, auto-detection of metadata from filename, URL validation via CheckRemoteUrl, persistence to local registry, and test coverage for parsing logic.
Out of Scope Changes check ✅ Passed All changes are within scope: new command implementation, supporting utility functions for URL parsing and validation, test coverage, and flag definitions are all necessary for the feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-agent-a6ccc8d2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new command, add-url, which allows users to add remote tarballs to the local registry by providing a URL. The implementation includes auto-detection of tarball metadata from the filename, with support for manual overrides via flags, and performs a URL accessibility check. My review identified several areas for improvement: the CheckRemoteUrl function should use an HTTP client with a timeout and leverage resp.ContentLength directly to avoid potential issues; the ParseTarballUrlInfo function should be more lenient when auto-detection fails to allow for manual overrides; the OS detection logic needs to be more consistent with the project's naming conventions; and there is a discrepancy regarding the target registry file for custom tarballs that needs to be addressed to prevent potential data loss or conflicts.

Comment on lines +573 to +593
func CheckRemoteUrl(remoteUrl string) (int64, error) {
// Try HEAD first to avoid downloading the file
// #nosec G107
resp, err := http.Head(remoteUrl)
if err == nil {
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
if resp.StatusCode == http.StatusOK {
var size int64
for key := range resp.Header {
if strings.EqualFold(key, "Content-Length") && len(resp.Header[key]) > 0 {
size, _ = strconv.ParseInt(resp.Header[key][0], 10, 0)
}
}
return size, nil
}
}
// Fall back to GET if HEAD failed or returned non-200
return checkRemoteUrl(remoteUrl)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The CheckRemoteUrl function uses the default HTTP client which has no timeout, potentially causing the CLI to hang on slow or unresponsive servers. Additionally, manually parsing the Content-Length header is unnecessary and error-prone (especially with bitSize set to 0 in ParseInt, which can overflow on 32-bit systems for files > 2GB). It is recommended to use a client with a timeout and leverage resp.ContentLength directly.

func CheckRemoteUrl(remoteUrl string) (int64, error) {
	client := &http.Client{
		Timeout: 10 * time.Second,
	}
	// Try HEAD first to avoid downloading the file
	// #nosec G107
	resp, err := client.Head(remoteUrl)
	if err == nil {
		defer func(Body io.ReadCloser) {
			_ = Body.Close()
		}(resp.Body)
		if resp.StatusCode == http.StatusOK {
			return resp.ContentLength, nil
		}
	}
	// Fall back to GET if HEAD failed or returned non-200
	return checkRemoteUrl(remoteUrl)
}

Comment on lines +604 to +607
flavor, version, shortVersion, err := common.FindTarballInfo(fileName)
if err != nil {
return TarballDescription{}, fmt.Errorf("could not parse version from filename %q: %s", fileName, err)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Returning an error here when FindTarballInfo fails prevents the user from using the override flags (--version, --flavor, etc.) for URLs with non-standard filenames. Since the caller already validates the required fields after applying overrides, this function should be more lenient and allow the process to continue even if auto-detection fails.

Suggested change
flavor, version, shortVersion, err := common.FindTarballInfo(fileName)
if err != nil {
return TarballDescription{}, fmt.Errorf("could not parse version from filename %q: %s", fileName, err)
}
flavor, version, shortVersion, _ := common.FindTarballInfo(fileName)

Comment on lines +638 to +644
OS = runtime.GOOS
if OS == "darwin" {
OS = "Darwin"
} else if OS == "linux" {
OS = "Linux"
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The default OS detection returns lowercase runtime.GOOS (e.g., "windows"), which is inconsistent with the explicit check for "Windows" (Title case) at line 635 and the general convention in this project for other operating systems like "Linux" and "Darwin".

	default:
		OS = runtime.GOOS
		if strings.EqualFold(OS, "darwin") {
			OS = "Darwin"
		} else if strings.EqualFold(OS, "linux") {
			OS = "Linux"
		} else if strings.EqualFold(OS, "windows") {
			OS = "Windows"
		}

Comment thread cmd/downloads.go
if err != nil {
return fmt.Errorf("error writing tarball list: %s", err)
}
fmt.Printf("Tarball below added to %s\n", downloads.TarballFileRegistry)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is a discrepancy between the implementation and the PR description. The description states that custom tarballs are saved to ~/.dbdeployer/custom_tarballs.json, but the code uses downloads.TarballFileRegistry, which points to tarball-list.json. This means custom entries are mixed into the main registry file, which might lead to them being lost or causing conflicts if the main registry is updated or reset.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
downloads/remote_registry.go (1)

573-593: Consider adding a timeout to HTTP requests.

The http.Head and the fallback http.Get use the default HTTP client, which has no timeout. This can cause indefinite hangs when checking URLs for unresponsive servers, degrading user experience.

♻️ Suggested fix: use a client with timeout
 func CheckRemoteUrl(remoteUrl string) (int64, error) {
 	// Try HEAD first to avoid downloading the file
+	client := &http.Client{Timeout: 30 * time.Second}
 	// `#nosec` G107
-	resp, err := http.Head(remoteUrl)
+	resp, err := client.Head(remoteUrl)
 	if err == nil {
 		defer func(Body io.ReadCloser) {
 			_ = Body.Close()
 		}(resp.Body)

Apply the same timeout to the checkRemoteUrl function for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@downloads/remote_registry.go` around lines 573 - 593, The CheckRemoteUrl
function (and its fallback checkRemoteUrl) currently use the default http client
with no timeout; update both to use an http.Client with a sensible Timeout
(e.g., a few seconds) and call client.Head / client.Get instead of http.Head /
http.Get so requests cannot hang indefinitely; ensure you still close resp.Body
in CheckRemoteUrl and mirror the same client/timeout usage in the checkRemoteUrl
helper for consistency (reference functions: CheckRemoteUrl and checkRemoteUrl).
cmd/downloads.go (1)

680-681: Pre-existing pattern: local registry file not loaded before modification.

The code uses downloads.DefaultTarballRegistry (the embedded registry) rather than first loading any existing ~/.dbdeployer/tarball-list.json. This follows the same pattern as addTarballToCollection, addRemoteTarballToCollection, and addTarballToCollectionFromStdin.

However, this means if a user previously ran downloads import to customize their registry, running add-url will overwrite their file with the embedded list plus the new entry, potentially losing their customizations.

This is pre-existing behavior, but worth noting for a future improvement.

♻️ Potential fix (for future consideration)
// Load existing collection - prefer local file if it exists
var tarballCollection downloads.TarballCollection
if downloads.TarballRegistryFileExist() {
    var err error
    tarballCollection, err = downloads.ReadTarballFileInfo()
    if err != nil {
        return fmt.Errorf("error reading existing tarball registry: %s", err)
    }
} else {
    tarballCollection = downloads.DefaultTarballRegistry
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/downloads.go` around lines 680 - 681, The code currently assigns
downloads.DefaultTarballRegistry to tarballCollection without attempting to load
a user-local registry, which can overwrite user customizations; change the
assignment in the add-url flow to first check
downloads.TarballRegistryFileExist() and if true call
downloads.ReadTarballFileInfo() (handling and returning any error) to populate
tarballCollection, otherwise fall back to downloads.DefaultTarballRegistry—this
mirrors the behavior used in addTarballToCollection,
addRemoteTarballToCollection, and addTarballToCollectionFromStdin.
downloads/remote_registry_test.go (1)

220-282: Good test coverage; consider adding MariaDB test case.

The tests cover MySQL and Percona patterns well. Consider adding a MariaDB test case to ensure the flavor detection works correctly for MariaDB's naming convention (e.g., mariadb-10.6.12-linux-systemd-x86_64.tar.gz), as it's a supported flavor in dbdeployer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@downloads/remote_registry_test.go` around lines 220 - 282, Add a MariaDB test
case to the TestParseTarballUrlInfo tests slice: insert a struct with name like
"mariadb-linux-amd64", url e.g.
"https://downloads.mariadb.org/f/mariadb-10.6.12/mariadb-10.6.12-linux-systemd-x86_64.tar.gz",
wantName "mariadb-10.6.12-linux-systemd-x86_64.tar.gz", wantVersion "10.6.12",
wantShort "10.6", wantOS "Linux", wantArch "amd64", wantFlavor "mariadb", and
wantMinimal false so the flavor detection in TestParseTarballUrlInfo validates
MariaDB naming; add it alongside the existing cases in the tests slice.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmd/downloads.go`:
- Around line 680-681: The code currently assigns
downloads.DefaultTarballRegistry to tarballCollection without attempting to load
a user-local registry, which can overwrite user customizations; change the
assignment in the add-url flow to first check
downloads.TarballRegistryFileExist() and if true call
downloads.ReadTarballFileInfo() (handling and returning any error) to populate
tarballCollection, otherwise fall back to downloads.DefaultTarballRegistry—this
mirrors the behavior used in addTarballToCollection,
addRemoteTarballToCollection, and addTarballToCollectionFromStdin.

In `@downloads/remote_registry_test.go`:
- Around line 220-282: Add a MariaDB test case to the TestParseTarballUrlInfo
tests slice: insert a struct with name like "mariadb-linux-amd64", url e.g.
"https://downloads.mariadb.org/f/mariadb-10.6.12/mariadb-10.6.12-linux-systemd-x86_64.tar.gz",
wantName "mariadb-10.6.12-linux-systemd-x86_64.tar.gz", wantVersion "10.6.12",
wantShort "10.6", wantOS "Linux", wantArch "amd64", wantFlavor "mariadb", and
wantMinimal false so the flavor detection in TestParseTarballUrlInfo validates
MariaDB naming; add it alongside the existing cases in the tests slice.

In `@downloads/remote_registry.go`:
- Around line 573-593: The CheckRemoteUrl function (and its fallback
checkRemoteUrl) currently use the default http client with no timeout; update
both to use an http.Client with a sensible Timeout (e.g., a few seconds) and
call client.Head / client.Get instead of http.Head / http.Get so requests cannot
hang indefinitely; ensure you still close resp.Body in CheckRemoteUrl and mirror
the same client/timeout usage in the checkRemoteUrl helper for consistency
(reference functions: CheckRemoteUrl and checkRemoteUrl).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 05df877b-0fb5-425b-9e30-fc97ecc94b41

📥 Commits

Reviewing files that changed from the base of the PR and between 437c392 and 13e1a1e.

📒 Files selected for processing (4)
  • cmd/downloads.go
  • downloads/remote_registry.go
  • downloads/remote_registry_test.go
  • globals/globals.go

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new dbdeployer downloads add-url <url> CLI flow to register remote tarball URLs into the local tarball registry, with filename-based metadata detection and optional URL accessibility verification.

Changes:

  • Introduces downloads add-url command with override flags and optional URL verification.
  • Adds URL/filename parsing helpers for auto-detecting flavor/version/OS/arch/minimal.
  • Adds unit tests for tarball URL parsing.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
globals/globals.go Adds a new flag label for skipping URL verification.
downloads/remote_registry.go Adds exported URL-check helper plus tarball URL parsing + OS/arch detection helpers.
downloads/remote_registry_test.go Adds tests covering common tarball URL patterns (MySQL/Percona/macOS/minimal).
cmd/downloads.go Implements new downloads add-url Cobra command and wires flags/registry write flow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +598 to +602
func ParseTarballUrlInfo(tarballUrl string) (TarballDescription, error) {
fileName := common.BaseName(tarballUrl)
if fileName == "" {
return TarballDescription{}, fmt.Errorf("could not determine filename from URL: %s", tarballUrl)
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParseTarballUrlInfo uses common.BaseName(tarballUrl) (filepath.Base) to extract the filename. For URLs with query strings (e.g. signed URLs like .../mysql-8.4.8.tar.xz?token=...) this will include the query and break FindTarballInfo parsing. Consider parsing with net/url and using path.Base(u.Path) (and/or stripping ? / #) before attempting tarball detection.

Copilot uses AI. Check for mistakes.
Comment on lines +573 to +592
func CheckRemoteUrl(remoteUrl string) (int64, error) {
// Try HEAD first to avoid downloading the file
// #nosec G107
resp, err := http.Head(remoteUrl)
if err == nil {
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
if resp.StatusCode == http.StatusOK {
var size int64
for key := range resp.Header {
if strings.EqualFold(key, "Content-Length") && len(resp.Header[key]) > 0 {
size, _ = strconv.ParseInt(resp.Header[key][0], 10, 0)
}
}
return size, nil
}
}
// Fall back to GET if HEAD failed or returned non-200
return checkRemoteUrl(remoteUrl)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CheckRemoteUrl (and the underlying checkRemoteUrl fallback) uses the default http client (http.Head/http.Get) with no timeout, so downloads add-url can hang indefinitely on slow/stalled connections. Consider using an http.Client with a reasonable Timeout (or request context with deadline) and reusing it for both HEAD/GET attempts.

Copilot uses AI. Check for mistakes.
Comment on lines +581 to +588
if resp.StatusCode == http.StatusOK {
var size int64
for key := range resp.Header {
if strings.EqualFold(key, "Content-Length") && len(resp.Header[key]) > 0 {
size, _ = strconv.ParseInt(resp.Header[key][0], 10, 0)
}
}
return size, nil
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In CheckRemoteUrl, Content-Length extraction iterates headers and uses strconv.ParseInt(..., 10, 0). Two improvements: (1) use resp.ContentLength (already parsed by net/http) or Header.Get("Content-Length") instead of looping, and (2) parse with bitSize 64 to avoid int-size-dependent overflow. This also reduces duplicated logic vs checkRemoteUrl().

Copilot uses AI. Check for mistakes.
Comment thread cmd/downloads.go
Comment on lines +641 to +642
if overrideMinimal {
tarballDesc.Minimal = true
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --minimal override currently can only force Minimal=true. If the filename auto-detect sets Minimal=true, there is no way to override it back to false (even --minimal=false won’t work) because the code only applies the override when overrideMinimal is true. Consider checking whether the flag was explicitly set (Flags().Lookup(...).Changed) and then assigning tarballDesc.Minimal = overrideMinimal.

Suggested change
if overrideMinimal {
tarballDesc.Minimal = true
// Only override the Minimal field if the --minimal flag was explicitly set.
if minimalFlag := cmd.Flags().Lookup("minimal"); minimalFlag != nil && minimalFlag.Changed {
tarballDesc.Minimal = overrideMinimal

Copilot uses AI. Check for mistakes.
Comment thread cmd/downloads.go
Comment on lines +680 to +706
// Load existing collection and add the new entry
var tarballCollection = downloads.DefaultTarballRegistry

existingTarball, findErr := downloads.FindTarballByName(tarballDesc.Name)
if findErr == nil {
if overwrite {
var newList []downloads.TarballDescription
newList, err = downloads.DeleteTarball(tarballCollection.Tarballs, tarballDesc.Name)
if err != nil {
return fmt.Errorf("error removing existing tarball %s from list: %s", tarballDesc.Name, err)
}
tarballCollection.Tarballs = newList
} else {
ops.DisplayTarball(existingTarball)
fmt.Println()
return fmt.Errorf("tarball %s already in the list; use --overwrite to replace it", tarballDesc.Name)
}
}

tarballCollection.Tarballs = append(tarballCollection.Tarballs, tarballDesc)

err = downloads.WriteTarballFileInfo(tarballCollection)
if err != nil {
return fmt.Errorf("error writing tarball list: %s", err)
}
fmt.Printf("Tarball below added to %s\n", downloads.TarballFileRegistry)
ops.DisplayTarball(tarballDesc)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says custom URLs are saved to ~/.dbdeployer/custom_tarballs.json and merged with the embedded registry at runtime, but this implementation writes to downloads.TarballFileRegistry (tarball-list.json) via WriteTarballFileInfo and relies on replacing DefaultTarballRegistry on startup. Either update the PR description/command help to match the existing tarball-list behavior, or implement a separate custom registry file + merge logic so users don’t pin themselves to an old full registry snapshot.

Copilot uses AI. Check for mistakes.
@renecannao renecannao merged commit eb40fa7 into master Apr 2, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Add 'downloads add-url' command for custom tarballs

2 participants