Implemented symlinks and mimetype for serve
This commit is contained in:
parent
523e519a3b
commit
605dc5352c
5 changed files with 81 additions and 16 deletions
|
|
@ -5,6 +5,9 @@ japanese for *stairs*. a minimal, fast file index server.
|
|||
single static binary. TOML config. sortable listings (name, type, size,
|
||||
modified).
|
||||
|
||||
browser-renderable files (images, audio, video, text, PDF and friends) are
|
||||
served inline so they open in place; everything else is sent as a download.
|
||||
|
||||
## build
|
||||
|
||||
```sh
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ footer a:hover {
|
|||
{{if .HasParent}}<tr><td><a href="../">../</a></td><td class="num ty">-</td><td class="num sz"></td><td class="num mod"></td></tr>{{end}}
|
||||
{{range .Rows}}
|
||||
<tr{{if .IsDir}} class="dir"{{end}}>
|
||||
<td><a href="{{.URL}}"{{if not .IsDir}} download{{end}}>{{.Name}}</a></td>
|
||||
<td><a href="{{.URL}}">{{.Name}}</a></td>
|
||||
<td class="num ty">{{.Type}}</td>
|
||||
<td class="num sz">{{if .IsDir}}-{{else}}{{.SizeS}}{{end}}</td>
|
||||
<td class="num mod">{{.ModS}}</td>
|
||||
|
|
|
|||
17
config.go
17
config.go
|
|
@ -10,10 +10,11 @@ import (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
Listen string `toml:"listen"`
|
||||
Root string `toml:"root"`
|
||||
Hidden bool `toml:"hidden"`
|
||||
Footer string `toml:"footer"`
|
||||
Listen string `toml:"listen"`
|
||||
Root string `toml:"root"`
|
||||
Hidden bool `toml:"hidden"`
|
||||
Footer string `toml:"footer"`
|
||||
FollowSymlinks bool `toml:"follow_symlinks"`
|
||||
}
|
||||
|
||||
func loadConfig(path string) (*Config, error) {
|
||||
|
|
@ -22,9 +23,15 @@ func loadConfig(path string) (*Config, error) {
|
|||
return nil, err
|
||||
}
|
||||
cfg := &Config{}
|
||||
if err := toml.Unmarshal(b, cfg); err != nil {
|
||||
md, err := toml.Decode(string(b), cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
// follow_symlinks defaults to true: symlinks are served as a window onto
|
||||
// their target, even outside root.
|
||||
if !md.IsDefined("follow_symlinks") {
|
||||
cfg.FollowSymlinks = true
|
||||
}
|
||||
if cfg.Root == "" {
|
||||
return nil, errors.New(`config: "root" is required`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,3 +11,7 @@ hidden = true
|
|||
|
||||
# footer shown at the bottom of every page
|
||||
footer = "kaidan 1.0"
|
||||
|
||||
# follow symlinks even when their target lies outside root. set to false to
|
||||
# keep the served tree confined to root (escaping symlinks then 404).
|
||||
follow_symlinks = true
|
||||
|
|
|
|||
71
server.go
71
server.go
|
|
@ -5,6 +5,7 @@ import (
|
|||
_ "embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
|
@ -30,6 +31,7 @@ type server struct {
|
|||
rootReal string // symlink-resolved root, used for containment checks
|
||||
hidden bool
|
||||
footer string
|
||||
follow bool // serve symlinks even when they point outside root
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
|
|
@ -43,6 +45,7 @@ func newServer(cfg *Config) *server {
|
|||
rootReal: rootReal,
|
||||
hidden: cfg.Hidden,
|
||||
footer: cfg.Footer,
|
||||
follow: cfg.FollowSymlinks,
|
||||
tpl: template.Must(template.New("index").Parse(indexHTML)),
|
||||
}
|
||||
}
|
||||
|
|
@ -111,7 +114,7 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if realPath != s.rootReal && !strings.HasPrefix(realPath, s.rootReal+string(os.PathSeparator)) {
|
||||
if !s.follow && realPath != s.rootReal && !strings.HasPrefix(realPath, s.rootReal+string(os.PathSeparator)) {
|
||||
http.NotFound(w, r) // symlink escaping the served root
|
||||
return
|
||||
}
|
||||
|
|
@ -147,11 +150,48 @@ func (s *server) serveFile(w http.ResponseWriter, r *http.Request, path, name st
|
|||
return
|
||||
}
|
||||
defer f.Close()
|
||||
disp := "attachment"
|
||||
if inlineFile(f, name) {
|
||||
disp = "inline"
|
||||
}
|
||||
w.Header().Set("Content-Disposition",
|
||||
mime.FormatMediaType("attachment", map[string]string{"filename": name}))
|
||||
mime.FormatMediaType(disp, map[string]string{"filename": name}))
|
||||
http.ServeContent(w, r, name, info.ModTime(), f)
|
||||
}
|
||||
|
||||
func inlineFile(f *os.File, name string) bool {
|
||||
if ct := mime.TypeByExtension(strings.ToLower(filepath.Ext(name))); ct != "" {
|
||||
return inlineMIME(ct)
|
||||
}
|
||||
buf := make([]byte, 512)
|
||||
n, err := f.Read(buf)
|
||||
if _, serr := f.Seek(0, io.SeekStart); serr != nil {
|
||||
return false
|
||||
}
|
||||
if err != nil && err != io.EOF {
|
||||
return false
|
||||
}
|
||||
return inlineMIME(http.DetectContentType(buf[:n]))
|
||||
}
|
||||
|
||||
func inlineMIME(ct string) bool {
|
||||
if i := strings.IndexByte(ct, ';'); i >= 0 {
|
||||
ct = strings.TrimSpace(ct[:i])
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(ct, "image/"),
|
||||
strings.HasPrefix(ct, "audio/"),
|
||||
strings.HasPrefix(ct, "video/"),
|
||||
strings.HasPrefix(ct, "text/"):
|
||||
return ct != "text/html"
|
||||
case ct == "application/pdf",
|
||||
ct == "application/xml",
|
||||
ct == "application/json":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *server) serveListing(w http.ResponseWriter, r *http.Request, dirPath, urlPath string) {
|
||||
entries, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
|
|
@ -176,20 +216,31 @@ func (s *server) serveListing(w http.ResponseWriter, r *http.Request, dirPath, u
|
|||
continue
|
||||
}
|
||||
isDir := e.IsDir()
|
||||
var size int64
|
||||
var mod time.Time
|
||||
if info, err := e.Info(); err == nil { // lstat: does not follow symlinks
|
||||
mod = info.ModTime()
|
||||
if !isDir {
|
||||
size = info.Size()
|
||||
}
|
||||
}
|
||||
if e.Type()&os.ModeSymlink != 0 {
|
||||
if ti, err := os.Stat(filepath.Join(dirPath, name)); err == nil {
|
||||
isDir = ti.IsDir()
|
||||
mod = ti.ModTime()
|
||||
if isDir {
|
||||
size = 0
|
||||
} else {
|
||||
size = ti.Size()
|
||||
}
|
||||
}
|
||||
}
|
||||
typ := "-"
|
||||
if isDir {
|
||||
typ = "dir"
|
||||
} else if ext := strings.TrimPrefix(filepath.Ext(name), "."); ext != "" {
|
||||
typ = strings.ToLower(ext)
|
||||
}
|
||||
var size int64
|
||||
var mod time.Time
|
||||
if info, err := e.Info(); err == nil {
|
||||
mod = info.ModTime()
|
||||
if !isDir {
|
||||
size = info.Size()
|
||||
}
|
||||
}
|
||||
rows = append(rows, row{
|
||||
Name: name,
|
||||
URL: dirURL + url.PathEscape(name),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue