selfupdate: fix --version X.Y depending on ./ in the download site links

To find the latest patch release of a minor version, for example
`rclone selfupdate --version 1.75`, selfupdate searched the listing of
downloads.rclone.org for href="./vX.Y.Z/". The leading ./ is a detail
of how Caddy's file server writes its links. A listing which linked to
the same directories as "vX.Y.Z/", which is an equally valid relative
URL, made selfupdate fail with "could not find the minor release".

This makes the ./ optional in the pattern, so selfupdate no longer
depends on which program wrote the listing, and takes the version from
a capture group rather than from fixed offsets into the match.

The listings written by rclone index now include the ./ for the
benefit of rclone versions without this fix, so this is to remove the
dependency for the future.
This commit is contained in:
Nick Craig-Wood committed 2026-09-30 12:14:28 +01:00
1 parent 89521d875d
commit 8cd43746a3
2 files changed
+61 -7

No files matched your search

+17 -7
View File
@@ -133,18 +133,28 @@ func GetVersion(ctx context.Context, beta bool, version string) (newVersion, sit
if err != nil {
return "", siteURL, fmt.Errorf("failed to get list of releases: %w", err)
}
reSubver := fmt.Sprintf(`href="\./%s\.\d+/"`, regexp.QuoteMeta(newVersion))
allSubvers := regexp.MustCompile(reSubver).FindAllString(string(html), -1)
if allSubvers == nil {
return "", siteURL, errors.New("could not find the minor release")
newVersion, err = latestMicroRelease(html, newVersion)
if err != nil {
return "", siteURL, err
}
// Use the fact that releases in the index are sorted by date
lastSubver := allSubvers[len(allSubvers)-1]
newVersion = lastSubver[8 : len(lastSubver)-2]
}
return
}
// latestMicroRelease returns the latest micro release of the
// major.minor release version linked to from the HTML index of
// releases.
func latestMicroRelease(html []byte, version string) (string, error) {
// The links may or may not start with ./ depending on what made the index
reSubver := fmt.Sprintf(`href="(?:\./)?(%s\.\d+)/"`, regexp.QuoteMeta(version))
allSubvers := regexp.MustCompile(reSubver).FindAllSubmatch(html, -1)
if allSubvers == nil {
return "", errors.New("could not find the minor release")
}
// Use the fact that releases in the index are sorted by date
return string(allSubvers[len(allSubvers)-1][1]), nil
}
// InstallUpdate performs rclone self-update
func InstallUpdate(ctx context.Context, opt *Options) error {
// Find the latest release number
+44
View File
@@ -45,6 +45,50 @@ func TestGetVersion(t *testing.T) {
assert.Equal(t, "v1.52.3", resultVer)
}
func TestLatestMicroRelease(t *testing.T) {
for _, test := range []struct {
name string
html string
want string
}{
{
name: "caddy",
html: `<a href="./v1.51.0/">v1.51.0/</a>
<a href="./v1.52.0/">v1.52.0/</a>
<a href="./v1.52.3/">v1.52.3/</a>
<a href="./v1.53.0/">v1.53.0/</a>
<a href="./v1.52-DEV/">v1.52-DEV/</a>
<a href="./v1.52.3/rclone-v1.52.3-linux-amd64.zip">rclone</a>`,
want: "v1.52.3",
},
{
name: "no prefix",
html: `<a href="v1.51.0/">v1.51.0/</a>
<a href="v1.52.0/">v1.52.0/</a>
<a href="v1.52.3/">v1.52.3/</a>
<a href="v1.53.0/">v1.53.0/</a>`,
want: "v1.52.3",
},
{
name: "other prefix",
html: `<a href="../v1.52.0/">v1.52.0/</a>
<a href="https://example.com/v1.52.1/">v1.52.1/</a>
<a href="v1.52.2/index.html">v1.52.2/</a>
<a href="v1x52.3/">v1x52.3/</a>`,
},
} {
t.Run(test.name, func(t *testing.T) {
got, err := latestMicroRelease([]byte(test.html), "v1.52")
if test.want == "" {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, test.want, got)
})
}
}
func TestInstallOnLinux(t *testing.T) {
testy.SkipUnreliable(t)
if runtime.GOOS != "linux" {