fileserver: configure plain browse time format (#8084)

stabilise-http2-test^2
Steffen Busch 2026-11-04 08:05:11 +01:00 committed by GitHub
parent 7bb95a47b2
commit 644dbe695d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 189 additions and 3 deletions

View File

@ -0,0 +1,36 @@
:80
file_server {
browse {
plain_time_format "2006-01-02 15:04:05 MST"
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":80"
],
"routes": [
{
"handle": [
{
"browse": {
"plain_time_format": "2006-01-02 15:04:05 MST"
},
"handler": "file_server",
"hide": [
"./Caddyfile"
]
}
]
}
]
}
}
}
}
}

View File

@ -0,0 +1,9 @@
:80
file_server {
browse {
plain_time_format ""
}
}
----------
plain_time_format requires a non-empty time layout argument

View File

@ -0,0 +1,9 @@
:80
file_server {
browse {
plain_time_format 2006-01-02 15:04:05 MST
}
}
----------
plain_time_format accepts exactly one time layout argument; enclose layouts containing spaces in quotes

View File

@ -0,0 +1,9 @@
:80
file_server {
browse {
plain_time_format
}
}
----------
plain_time_format is missing a time layout argument

View File

@ -69,10 +69,17 @@ type Browse struct {
// FileLimit limits the number of up to n DirEntry values in directory order.
FileLimit int `json:"file_limit,omitempty"`
// PlainTimeFormat specifies the Go time layout used for modification
// times in plain-text directory listings. The default layout is
// `January 2, 2006 at 15:04:05`. See
// [Go's time layout documentation](https://pkg.go.dev/time#pkg-constants).
PlainTimeFormat string `json:"plain_time_format,omitempty"`
}
const (
defaultDirEntryLimit = 10000
defaultDirEntryLimit = 10000
defaultPlainTimeFormat = "January 2, 2006 at 15:04:05"
)
func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
@ -167,11 +174,19 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht
return caddyhttp.Error(http.StatusInternalServerError, err)
}
// Use the configured time format if specified, otherwise use the default
plainTimeFormat := defaultPlainTimeFormat
if fsrv.Browse.PlainTimeFormat != "" {
plainTimeFormat = fsrv.Browse.PlainTimeFormat
}
// Actual files
for _, item := range listing.Items {
//nolint:gosec // not sure how this could be XSS unless you lose control of the file system (like aren't sanitizing) and client ignores Content-Type of text/plain
if _, err := fmt.Fprintf(writer, "%s\t%s\t%s\n",
item.Name, item.HumanSize(), item.HumanModTime("January 2, 2006 at 15:04:05"),
item.Name,
item.HumanSize(),
item.HumanModTime(plainTimeFormat),
); err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err)
}

View File

@ -0,0 +1,88 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fileserver
import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func TestBrowsePlainTimeFormat(t *testing.T) {
root := t.TempDir()
filename := filepath.Join(root, "example.txt")
if err := os.WriteFile(filename, nil, 0o600); err != nil {
t.Fatal(err)
}
modTime := time.Date(2024, time.January, 2, 3, 4, 5, 0, time.UTC)
if err := os.Chtimes(filename, modTime, modTime); err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name string
layout string
wantModTime string
}{
{
name: "default",
wantModTime: "January 2, 2024 at 03:04:05",
},
{
name: "custom",
layout: "2006-01-02T15:04:05Z07:00",
wantModTime: "2024-01-02T03:04:05Z",
},
} {
t.Run(tc.name, func(t *testing.T) {
fsrv := FileServer{
Root: root,
Browse: &Browse{
PlainTimeFormat: tc.layout,
},
}
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
if err := fsrv.Provision(ctx); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
req.Header.Set("Accept", "text/plain")
originalReq := *req
reqCtx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, caddy.NewReplacer())
reqCtx = context.WithValue(reqCtx, caddyhttp.OriginalRequestCtxKey, originalReq)
req = req.WithContext(reqCtx)
w := httptest.NewRecorder()
if err := fsrv.ServeHTTP(w, req, nil); err != nil {
t.Fatal(err)
}
if got := w.Header().Get("Content-Type"); got != "text/plain; charset=utf-8" {
t.Fatalf("Content-Type = %q, want %q", got, "text/plain; charset=utf-8")
}
if !strings.Contains(w.Body.String(), tc.wantModTime) {
t.Fatalf("response body does not contain modification time %q:\n%s", tc.wantModTime, w.Body.String())
}
})
}
}

View File

@ -56,7 +56,12 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
// root <path>
// hide <files...>
// index <files...>
// browse [<template_file>]
// browse [<template_file>] {
// reveal_symlinks
// sort <options...>
// file_limit <limit>
// plain_time_format <layout>
// }
// precompressed <formats...>
// status <status>
// disable_canonical_uris
@ -147,6 +152,21 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.Err("file_limit is already enabled")
}
fsrv.Browse.FileLimit = val
case "plain_time_format":
plainTimeFormat := d.RemainingArgs()
if len(plainTimeFormat) == 0 {
return d.Err("plain_time_format is missing a time layout argument")
}
if len(plainTimeFormat) > 1 {
return d.Err("plain_time_format accepts exactly one time layout argument; enclose layouts containing spaces in quotes")
}
if plainTimeFormat[0] == "" {
return d.Err("plain_time_format requires a non-empty time layout argument")
}
if fsrv.Browse.PlainTimeFormat != "" {
return d.Err("plain_time_format is already set")
}
fsrv.Browse.PlainTimeFormat = plainTimeFormat[0]
default:
return d.Errf("unknown subdirective '%s'", d.Val())
}