forked from caddyserver/caddy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
browse.go
251 lines (211 loc) · 5.2 KB
/
browse.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
// Package browse provides middleware for listing files in a directory
// when directory path is requested instead of a specific file.
package browse
import (
"bytes"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"strings"
"time"
"github.com/dustin/go-humanize"
"github.com/mholt/caddy/middleware"
)
// Browse is an http.Handler that can show a file listing when
// directories in the given paths are specified.
type Browse struct {
Next middleware.Handler
Root string
Configs []BrowseConfig
}
// BrowseConfig is a configuration for browsing in a particular path.
type BrowseConfig struct {
PathScope string
Template *template.Template
}
// A Listing is used to fill out a template.
type Listing struct {
// The name of the directory (the last element of the path)
Name string
// The full path of the request
Path string
// Whether the parent directory is browsable
CanGoUp bool
// The items (files and folders) in the path
Items []FileInfo
}
// FileInfo is the info about a particular file or directory
type FileInfo struct {
IsDir bool
Name string
Size int64
URL string
ModTime time.Time
Mode os.FileMode
}
func (fi FileInfo) HumanSize() string {
return humanize.Bytes(uint64(fi.Size))
}
func (fi FileInfo) HumanModTime(format string) string {
return fi.ModTime.Format(format)
}
var IndexPages = []string{
"index.html",
"index.htm",
"default.html",
"default.htm",
}
// New creates a new instance of browse middleware.
func New(c middleware.Controller) (middleware.Middleware, error) {
configs, err := parse(c)
if err != nil {
return nil, err
}
browse := Browse{
Root: c.Root(),
Configs: configs,
}
return func(next middleware.Handler) middleware.Handler {
browse.Next = next
return browse
}, nil
}
// ServeHTTP implements the middleware.Handler interface.
func (b Browse) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
filename := b.Root + r.URL.Path
info, err := os.Stat(filename)
if err != nil {
return b.Next.ServeHTTP(w, r)
}
if !info.IsDir() {
return b.Next.ServeHTTP(w, r)
}
// See if there's a browse configuration to match the path
for _, bc := range b.Configs {
if !middleware.Path(r.URL.Path).Matches(bc.PathScope) {
continue
}
// Browsing navigation gets messed up if browsing a directory
// that doesn't end in "/" (which it should, anyway)
if r.URL.Path[len(r.URL.Path)-1] != '/' {
http.Redirect(w, r, r.URL.Path+"/", http.StatusTemporaryRedirect)
return 0, nil
}
// Load directory contents
file, err := os.Open(b.Root + r.URL.Path)
if err != nil {
return http.StatusForbidden, err
}
defer file.Close()
files, err := file.Readdir(-1)
if err != nil {
return http.StatusForbidden, err
}
// Assemble listing of directory contents
var fileinfos []FileInfo
var abort bool // we bail early if we find an index file
for _, f := range files {
name := f.Name()
// Directory is not browseable if it contains index file
for _, indexName := range IndexPages {
if name == indexName {
abort = true
break
}
}
if abort {
break
}
if f.IsDir() {
name += "/"
}
url := url.URL{Path: name}
fileinfos = append(fileinfos, FileInfo{
IsDir: f.IsDir(),
Name: f.Name(),
Size: f.Size(),
URL: url.String(),
ModTime: f.ModTime(),
Mode: f.Mode(),
})
}
if abort {
// this dir has an index file, so not browsable
continue
}
// Determine if user can browse up another folder
var canGoUp bool
curPath := strings.TrimSuffix(r.URL.Path, "/")
for _, other := range b.Configs {
if strings.HasPrefix(path.Dir(curPath), other.PathScope) {
canGoUp = true
break
}
}
listing := Listing{
Name: path.Base(r.URL.Path),
Path: r.URL.Path,
CanGoUp: canGoUp,
Items: fileinfos,
}
var buf bytes.Buffer
err = bc.Template.Execute(&buf, listing)
if err != nil {
return http.StatusInternalServerError, err
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
buf.WriteTo(w)
return http.StatusOK, nil
}
// Didn't qualify; pass-thru
return b.Next.ServeHTTP(w, r)
}
// parse returns a list of browsing configurations
func parse(c middleware.Controller) ([]BrowseConfig, error) {
var configs []BrowseConfig
appendCfg := func(bc BrowseConfig) error {
for _, c := range configs {
if c.PathScope == bc.PathScope {
return fmt.Errorf("Duplicate browsing config for %s", c.PathScope)
}
}
configs = append(configs, bc)
return nil
}
for c.Next() {
var bc BrowseConfig
// First argument is directory to allow browsing; default is site root
if c.NextArg() {
bc.PathScope = c.Val()
} else {
bc.PathScope = "/"
}
// Second argument would be the template file to use
var tplText string
if c.NextArg() {
tplBytes, err := ioutil.ReadFile(c.Val())
if err != nil {
return configs, err
}
tplText = string(tplBytes)
} else {
tplText = defaultTemplate
}
// Build the template
tpl, err := template.New("listing").Parse(tplText)
if err != nil {
return configs, err
}
bc.Template = tpl
// Save configuration
err = appendCfg(bc)
if err != nil {
return configs, err
}
}
return configs, nil
}