@marcel91200 / caddy Mirror · read-only / commits / 644dbe69
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

fileserver: configure plain browse time format (#8084)

Steffen Busch committed Nov 4, 2026 at 08:05 UTC 644dbe695d295a5f2feef017d9f04b3220aef2b0
7 files changed +189 -3
caddytest/integration/caddyfile_adapt/file_server_plain_time_format.caddyfiletest new
+36
@@ -0,0 +1,36 @@
1 +:80
2 +
3 +file_server {
4 + browse {
5 + plain_time_format "2006-01-02 15:04:05 MST"
6 + }
7 +}
8 +----------
9 +{
10 + "apps": {
11 + "http": {
12 + "servers": {
13 + "srv0": {
14 + "listen": [
15 + ":80"
16 + ],
17 + "routes": [
18 + {
19 + "handle": [
20 + {
21 + "browse": {
22 + "plain_time_format": "2006-01-02 15:04:05 MST"
23 + },
24 + "handler": "file_server",
25 + "hide": [
26 + "./Caddyfile"
27 + ]
28 + }
29 + ]
30 + }
31 + ]
32 + }
33 + }
34 + }
35 + }
36 +}
caddytest/integration/caddyfile_adapt/file_server_plain_time_format_empty_argument.caddyfiletest new
+9
@@ -0,0 +1,9 @@
1 +:80
2 +
3 +file_server {
4 + browse {
5 + plain_time_format ""
6 + }
7 +}
8 +----------
9 +plain_time_format requires a non-empty time layout argument
caddytest/integration/caddyfile_adapt/file_server_plain_time_format_extra_arguments.caddyfiletest new
+9
@@ -0,0 +1,9 @@
1 +:80
2 +
3 +file_server {
4 + browse {
5 + plain_time_format 2006-01-02 15:04:05 MST
6 + }
7 +}
8 +----------
9 +plain_time_format accepts exactly one time layout argument; enclose layouts containing spaces in quotes
caddytest/integration/caddyfile_adapt/file_server_plain_time_format_missing_argument.caddyfiletest new
+9
@@ -0,0 +1,9 @@
1 +:80
2 +
3 +file_server {
4 + browse {
5 + plain_time_format
6 + }
7 +}
8 +----------
9 +plain_time_format is missing a time layout argument
modules/caddyhttp/fileserver/browse.go
+17 -2
@@ -69,10 +69,17 @@ type Browse struct {
69
70 // FileLimit limits the number of up to n DirEntry values in directory order.
71 FileLimit int `json:"file_limit,omitempty"`
72 +
73 + // PlainTimeFormat specifies the Go time layout used for modification
74 + // times in plain-text directory listings. The default layout is
75 + // `January 2, 2006 at 15:04:05`. See
76 + // [Go's time layout documentation](https://pkg.go.dev/time#pkg-constants).
77 + PlainTimeFormat string `json:"plain_time_format,omitempty"`
78 }
79
80 const (
75 - defaultDirEntryLimit = 10000
81 + defaultDirEntryLimit = 10000
82 + defaultPlainTimeFormat = "January 2, 2006 at 15:04:05"
83 )
84
85 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
174 return caddyhttp.Error(http.StatusInternalServerError, err)
175 }
176
177 + // Use the configured time format if specified, otherwise use the default
178 + plainTimeFormat := defaultPlainTimeFormat
179 + if fsrv.Browse.PlainTimeFormat != "" {
180 + plainTimeFormat = fsrv.Browse.PlainTimeFormat
181 + }
182 +
183 // Actual files
184 for _, item := range listing.Items {
185 //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
186 if _, err := fmt.Fprintf(writer, "%s\t%s\t%s\n",
174 - item.Name, item.HumanSize(), item.HumanModTime("January 2, 2006 at 15:04:05"),
187 + item.Name,
188 + item.HumanSize(),
189 + item.HumanModTime(plainTimeFormat),
190 ); err != nil {
191 return caddyhttp.Error(http.StatusInternalServerError, err)
192 }
modules/caddyhttp/fileserver/browse_test.go new
+88
@@ -0,0 +1,88 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package fileserver
16 +
17 +import (
18 + "context"
19 + "net/http"
20 + "net/http/httptest"
21 + "os"
22 + "path/filepath"
23 + "strings"
24 + "testing"
25 + "time"
26 +
27 + "github.com/caddyserver/caddy/v2"
28 + "github.com/caddyserver/caddy/v2/modules/caddyhttp"
29 +)
30 +
31 +func TestBrowsePlainTimeFormat(t *testing.T) {
32 + root := t.TempDir()
33 + filename := filepath.Join(root, "example.txt")
34 + if err := os.WriteFile(filename, nil, 0o600); err != nil {
35 + t.Fatal(err)
36 + }
37 + modTime := time.Date(2024, time.January, 2, 3, 4, 5, 0, time.UTC)
38 + if err := os.Chtimes(filename, modTime, modTime); err != nil {
39 + t.Fatal(err)
40 + }
41 +
42 + for _, tc := range []struct {
43 + name string
44 + layout string
45 + wantModTime string
46 + }{
47 + {
48 + name: "default",
49 + wantModTime: "January 2, 2024 at 03:04:05",
50 + },
51 + {
52 + name: "custom",
53 + layout: "2006-01-02T15:04:05Z07:00",
54 + wantModTime: "2024-01-02T03:04:05Z",
55 + },
56 + } {
57 + t.Run(tc.name, func(t *testing.T) {
58 + fsrv := FileServer{
59 + Root: root,
60 + Browse: &Browse{
61 + PlainTimeFormat: tc.layout,
62 + },
63 + }
64 + ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
65 + if err := fsrv.Provision(ctx); err != nil {
66 + t.Fatal(err)
67 + }
68 +
69 + req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
70 + req.Header.Set("Accept", "text/plain")
71 + originalReq := *req
72 + reqCtx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, caddy.NewReplacer())
73 + reqCtx = context.WithValue(reqCtx, caddyhttp.OriginalRequestCtxKey, originalReq)
74 + req = req.WithContext(reqCtx)
75 +
76 + w := httptest.NewRecorder()
77 + if err := fsrv.ServeHTTP(w, req, nil); err != nil {
78 + t.Fatal(err)
79 + }
80 + if got := w.Header().Get("Content-Type"); got != "text/plain; charset=utf-8" {
81 + t.Fatalf("Content-Type = %q, want %q", got, "text/plain; charset=utf-8")
82 + }
83 + if !strings.Contains(w.Body.String(), tc.wantModTime) {
84 + t.Fatalf("response body does not contain modification time %q:\n%s", tc.wantModTime, w.Body.String())
85 + }
86 + })
87 + }
88 +}
modules/caddyhttp/fileserver/caddyfile.go
+21 -1
@@ -56,7 +56,12 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
56 // root <path>
57 // hide <files...>
58 // index <files...>
59 -// browse [<template_file>]
59 +// browse [<template_file>] {
60 +// reveal_symlinks
61 +// sort <options...>
62 +// file_limit <limit>
63 +// plain_time_format <layout>
64 +// }
65 // precompressed <formats...>
66 // status <status>
67 // disable_canonical_uris
@@ -147,6 +152,21 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
152 return d.Err("file_limit is already enabled")
153 }
154 fsrv.Browse.FileLimit = val
155 + case "plain_time_format":
156 + plainTimeFormat := d.RemainingArgs()
157 + if len(plainTimeFormat) == 0 {
158 + return d.Err("plain_time_format is missing a time layout argument")
159 + }
160 + if len(plainTimeFormat) > 1 {
161 + return d.Err("plain_time_format accepts exactly one time layout argument; enclose layouts containing spaces in quotes")
162 + }
163 + if plainTimeFormat[0] == "" {
164 + return d.Err("plain_time_format requires a non-empty time layout argument")
165 + }
166 + if fsrv.Browse.PlainTimeFormat != "" {
167 + return d.Err("plain_time_format is already set")
168 + }
169 + fsrv.Browse.PlainTimeFormat = plainTimeFormat[0]
170 default:
171 return d.Errf("unknown subdirective '%s'", d.Val())
172 }