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:
BIN
cmd/serve/http/favicon.png
Normal file
BIN
cmd/serve/http/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 704 B |
@@ -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, "/")
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user