1
0
mirror of https://github.com/rclone/rclone.git synced 2026-02-26 01:13:32 +00:00

serve http: add fallback embedded favicon

Browsers make a request to /favicon.ico when visiting pages generated
by the HTTP server.

Previously, if remotes did not have a /favicon.ico then the server
responded with a 404, causing browsers to show a default icon.

This adds a tiny fallback embedded PNG rclone favicon to help users
identify the rclone browser tab.
This commit is contained in:
Leon Brocard
2026-02-25 13:48:17 +01:00
committed by GitHub
parent 36913cda94
commit be73a72f93
3 changed files with 76 additions and 0 deletions

BIN
cmd/serve/http/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

View File

@@ -3,6 +3,7 @@ package http
import (
"context"
_ "embed"
"errors"
"fmt"
"io"
@@ -57,6 +58,9 @@ var DefaultOpt = Options{
// Opt is options set by command line flags
var Opt = DefaultOpt
//go:embed favicon.png
var faviconData []byte
func init() {
fs.RegisterGlobalOptions(fs.OptionsInfo{Name: "http", Opt: &Opt, Options: OptionsInfo})
}
@@ -201,6 +205,7 @@ func newServer(ctx context.Context, f fs.Fs, opt *Options, vfsOpt *vfscommon.Opt
middleware.SetHeader("Accept-Ranges", "bytes"),
middleware.SetHeader("Server", "rclone/"+fs.Version),
)
router.Get("/favicon.ico", s.serveFavicon)
router.Get("/*", s.handler)
router.Head("/*", s.handler)
@@ -225,6 +230,27 @@ func (s *HTTP) Shutdown() error {
return s.server.Shutdown()
}
// serveFavicon serves the remote's favicon.ico if it exists, otherwise
// the rclone favicon
func (s *HTTP) serveFavicon(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
VFS, err := s.getVFS(ctx)
if err == nil {
node, err := VFS.Stat("favicon.ico")
if err == nil && node.IsFile() {
// Remote has favicon.ico, serve it as a regular file
s.serveFile(w, r, "favicon.ico")
return
}
}
// Serve the embedded rclone favicon
w.Header().Set("Content-Type", "image/png")
w.Header().Set("Cache-Control", "max-age=86400")
if _, err := w.Write(faviconData); err != nil {
fs.Debugf(nil, "Failed to write favicon: %v", err)
}
}
// handler reads incoming requests and dispatches them
func (s *HTTP) handler(w http.ResponseWriter, r *http.Request) {
isDir := strings.HasSuffix(r.URL.Path, "/")

View File

@@ -297,6 +297,56 @@ func TestAuthProxy(t *testing.T) {
testGET(t, true)
}
func TestFavicon(t *testing.T) {
ctx := context.Background()
doGet := func(testURL, path string) *http.Response {
req, err := http.NewRequest("GET", testURL+path, nil)
require.NoError(t, err)
req.SetBasicAuth(testUser, testPass)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
return resp
}
t.Run("fallback", func(t *testing.T) {
// testdata/files has no favicon.ico, so the embedded fallback is served
f, err := fs.NewFs(ctx, "testdata/files")
require.NoError(t, err)
s, testURL := start(ctx, t, f)
defer func() { assert.NoError(t, s.server.Shutdown()) }()
resp := doGet(testURL, "favicon.ico")
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "image/png", resp.Header.Get("Content-Type"))
assert.Equal(t, "max-age=86400", resp.Header.Get("Cache-Control"))
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, faviconData, body)
})
t.Run("remote override", func(t *testing.T) {
// Start a server on a temp dir that already contains a custom favicon.ico,
// so the VFS sees it at init time and serves it instead of the fallback.
dir := t.TempDir()
customFavicon := []byte("custom favicon data")
require.NoError(t, os.WriteFile(filepath.Join(dir, "favicon.ico"), customFavicon, 0666))
f, err := fs.NewFs(ctx, dir)
require.NoError(t, err)
s, testURL := start(ctx, t, f)
defer func() { assert.NoError(t, s.server.Shutdown()) }()
resp := doGet(testURL, "favicon.ico")
defer func() { _ = resp.Body.Close() }()
assert.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, customFavicon, body)
})
}
func TestRc(t *testing.T) {
servetest.TestRc(t, rc.Params{
"type": "http",