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,
|
single static binary. TOML config. sortable listings (name, type, size,
|
||||||
modified).
|
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
|
## build
|
||||||
|
|
||||||
```sh
|
```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}}
|
{{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}}
|
{{range .Rows}}
|
||||||
<tr{{if .IsDir}} class="dir"{{end}}>
|
<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 ty">{{.Type}}</td>
|
||||||
<td class="num sz">{{if .IsDir}}-{{else}}{{.SizeS}}{{end}}</td>
|
<td class="num sz">{{if .IsDir}}-{{else}}{{.SizeS}}{{end}}</td>
|
||||||
<td class="num mod">{{.ModS}}</td>
|
<td class="num mod">{{.ModS}}</td>
|
||||||
|
|
|
||||||
17
config.go
17
config.go
|
|
@ -10,10 +10,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Listen string `toml:"listen"`
|
Listen string `toml:"listen"`
|
||||||
Root string `toml:"root"`
|
Root string `toml:"root"`
|
||||||
Hidden bool `toml:"hidden"`
|
Hidden bool `toml:"hidden"`
|
||||||
Footer string `toml:"footer"`
|
Footer string `toml:"footer"`
|
||||||
|
FollowSymlinks bool `toml:"follow_symlinks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig(path string) (*Config, error) {
|
func loadConfig(path string) (*Config, error) {
|
||||||
|
|
@ -22,9 +23,15 @@ func loadConfig(path string) (*Config, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
cfg := &Config{}
|
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)
|
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 == "" {
|
if cfg.Root == "" {
|
||||||
return nil, errors.New(`config: "root" is required`)
|
return nil, errors.New(`config: "root" is required`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,7 @@ hidden = true
|
||||||
|
|
||||||
# footer shown at the bottom of every page
|
# footer shown at the bottom of every page
|
||||||
footer = "kaidan 1.0"
|
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"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
@ -30,6 +31,7 @@ type server struct {
|
||||||
rootReal string // symlink-resolved root, used for containment checks
|
rootReal string // symlink-resolved root, used for containment checks
|
||||||
hidden bool
|
hidden bool
|
||||||
footer string
|
footer string
|
||||||
|
follow bool // serve symlinks even when they point outside root
|
||||||
tpl *template.Template
|
tpl *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,6 +45,7 @@ func newServer(cfg *Config) *server {
|
||||||
rootReal: rootReal,
|
rootReal: rootReal,
|
||||||
hidden: cfg.Hidden,
|
hidden: cfg.Hidden,
|
||||||
footer: cfg.Footer,
|
footer: cfg.Footer,
|
||||||
|
follow: cfg.FollowSymlinks,
|
||||||
tpl: template.Must(template.New("index").Parse(indexHTML)),
|
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)
|
http.NotFound(w, r)
|
||||||
return
|
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
|
http.NotFound(w, r) // symlink escaping the served root
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -147,11 +150,48 @@ func (s *server) serveFile(w http.ResponseWriter, r *http.Request, path, name st
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
disp := "attachment"
|
||||||
|
if inlineFile(f, name) {
|
||||||
|
disp = "inline"
|
||||||
|
}
|
||||||
w.Header().Set("Content-Disposition",
|
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)
|
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) {
|
func (s *server) serveListing(w http.ResponseWriter, r *http.Request, dirPath, urlPath string) {
|
||||||
entries, err := os.ReadDir(dirPath)
|
entries, err := os.ReadDir(dirPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -176,20 +216,31 @@ func (s *server) serveListing(w http.ResponseWriter, r *http.Request, dirPath, u
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
isDir := e.IsDir()
|
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 := "-"
|
typ := "-"
|
||||||
if isDir {
|
if isDir {
|
||||||
typ = "dir"
|
typ = "dir"
|
||||||
} else if ext := strings.TrimPrefix(filepath.Ext(name), "."); ext != "" {
|
} else if ext := strings.TrimPrefix(filepath.Ext(name), "."); ext != "" {
|
||||||
typ = strings.ToLower(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{
|
rows = append(rows, row{
|
||||||
Name: name,
|
Name: name,
|
||||||
URL: dirURL + url.PathEscape(name),
|
URL: dirURL + url.PathEscape(name),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue