diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go
index 7602f3a9e..86adc7e39 100644
--- a/modules/caddyhttp/fileserver/browse.go
+++ b/modules/caddyhttp/fileserver/browse.go
@@ -50,6 +50,8 @@ var BrowseTemplate string
type Browse struct {
// Filename of the template to use instead of the embedded browse template.
TemplateFile string `json:"template_file,omitempty"`
+ // Determines whether or not targets of symlinks should be revealed.
+ RevealSymlinks bool `json:"reveal_symlinks,omitempty"`
}
func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
diff --git a/modules/caddyhttp/fileserver/browse.html b/modules/caddyhttp/fileserver/browse.html
index af1772bfd..484d90020 100644
--- a/modules/caddyhttp/fileserver/browse.html
+++ b/modules/caddyhttp/fileserver/browse.html
@@ -962,7 +962,15 @@ footer {
{{template "icon" .}}
+ {{- if not .SymlinkPath}}
{{html .Name}}
+ {{- else}}
+ {{html .Name}}
+
+
+
+ {{html .SymlinkPath}}
+ {{- end}}
{{- if .IsDir}}
diff --git a/modules/caddyhttp/fileserver/browsetplcontext.go b/modules/caddyhttp/fileserver/browsetplcontext.go
index 81db3d06b..5456f0597 100644
--- a/modules/caddyhttp/fileserver/browsetplcontext.go
+++ b/modules/caddyhttp/fileserver/browsetplcontext.go
@@ -20,6 +20,7 @@ import (
"net/url"
"os"
"path"
+ "path/filepath"
"sort"
"strconv"
"strings"
@@ -74,12 +75,21 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
size := info.Size()
fileIsSymlink := isSymlink(info)
+ symlinkPath := ""
if fileIsSymlink {
path := caddyhttp.SanitizedPathJoin(root, path.Join(urlPath, info.Name()))
fileInfo, err := fs.Stat(fileSystem, path)
if err == nil {
size = fileInfo.Size()
}
+
+ if fsrv.Browse.RevealSymlinks {
+ symLinkTarget, err := filepath.EvalSymlinks(path)
+ if err == nil {
+ symlinkPath = symLinkTarget
+ }
+ }
+
// An error most likely means the symlink target doesn't exist,
// which isn't entirely unusual and shouldn't fail the listing.
// In this case, just use the size of the symlink itself, which
@@ -93,14 +103,15 @@ func (fsrv *FileServer) directoryListing(ctx context.Context, fileSystem fs.FS,
u := url.URL{Path: "./" + name} // prepend with "./" to fix paths with ':' in the name
tplCtx.Items = append(tplCtx.Items, fileInfo{
- IsDir: isDir,
- IsSymlink: fileIsSymlink,
- Name: name,
- Size: size,
- URL: u.String(),
- ModTime: info.ModTime().UTC(),
- Mode: info.Mode(),
- Tpl: tplCtx, // a reference up to the template context is useful
+ IsDir: isDir,
+ IsSymlink: fileIsSymlink,
+ Name: name,
+ Size: size,
+ URL: u.String(),
+ ModTime: info.ModTime().UTC(),
+ Mode: info.Mode(),
+ Tpl: tplCtx, // a reference up to the template context is useful
+ SymlinkPath: symlinkPath,
})
}
@@ -230,13 +241,14 @@ type crumb struct {
// fileInfo contains serializable information
// about a file or directory.
type fileInfo struct {
- Name string `json:"name"`
- Size int64 `json:"size"`
- URL string `json:"url"`
- ModTime time.Time `json:"mod_time"`
- Mode os.FileMode `json:"mode"`
- IsDir bool `json:"is_dir"`
- IsSymlink bool `json:"is_symlink"`
+ Name string `json:"name"`
+ Size int64 `json:"size"`
+ URL string `json:"url"`
+ ModTime time.Time `json:"mod_time"`
+ Mode os.FileMode `json:"mode"`
+ IsDir bool `json:"is_dir"`
+ IsSymlink bool `json:"is_symlink"`
+ SymlinkPath string `json:"symlink_path,omitempty"`
// a pointer to the template context is useful inside nested templates
Tpl *browseTemplateContext `json:"-"`
diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go
index b18bcdcef..6ad9190f3 100644
--- a/modules/caddyhttp/fileserver/caddyfile.go
+++ b/modules/caddyhttp/fileserver/caddyfile.go
@@ -112,6 +112,15 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
fsrv.Browse = new(Browse)
d.Args(&fsrv.Browse.TemplateFile)
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
+ if d.Val() != "reveal_symlinks" {
+ return d.Errf("unknown subdirective '%s'", d.Val())
+ }
+ if fsrv.Browse.RevealSymlinks {
+ return d.Err("Symlinks path reveal is already enabled")
+ }
+ fsrv.Browse.RevealSymlinks = true
+ }
case "precompressed":
var order []string
diff --git a/modules/caddyhttp/fileserver/command.go b/modules/caddyhttp/fileserver/command.go
index 2bc816743..a76998405 100644
--- a/modules/caddyhttp/fileserver/command.go
+++ b/modules/caddyhttp/fileserver/command.go
@@ -39,7 +39,7 @@ import (
func init() {
caddycmd.RegisterCommand(caddycmd.Command{
Name: "file-server",
- Usage: "[--domain ] [--root ] [--listen ] [--browse] [--access-log] [--precompressed]",
+ Usage: "[--domain ] [--root ] [--listen ] [--browse] [--reveal-symlinks] [--access-log] [--precompressed]",
Short: "Spins up a production-ready file server",
Long: `
A simple but production-ready file server. Useful for quick deployments,
@@ -62,6 +62,7 @@ respond with a file listing.`,
cmd.Flags().StringP("root", "r", "", "The path to the root of the site")
cmd.Flags().StringP("listen", "l", "", "The address to which to bind the listener")
cmd.Flags().BoolP("browse", "b", false, "Enable directory browsing")
+ cmd.Flags().BoolP("reveal-symlinks", "", false, "Show symlink paths when browse is enabled.")
cmd.Flags().BoolP("templates", "t", false, "Enable template rendering")
cmd.Flags().BoolP("access-log", "a", false, "Enable the access log")
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
@@ -91,12 +92,12 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
templates := fs.Bool("templates")
accessLog := fs.Bool("access-log")
debug := fs.Bool("debug")
+ revealSymlinks := fs.Bool("reveal-symlinks")
compress := !fs.Bool("no-compress")
precompressed, err := fs.GetStringSlice("precompressed")
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid precompressed flag: %v", err)
}
-
var handlers []json.RawMessage
if compress {
@@ -150,7 +151,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
}
if browse {
- handler.Browse = new(Browse)
+ handler.Browse = &Browse{RevealSymlinks: revealSymlinks}
}
handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "file_server", nil))