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
}