Files
old-ibcetvorke/vendor/github.com/a-h/gemini/filesystem.go
2023-07-30 19:21:16 +02:00

129 lines
3.7 KiB
Go

package gemini
import (
"errors"
"fmt"
"io"
"mime"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strings"
"github.com/a-h/gemini/log"
)
type Dir string
// Open implements FileSystem using os.Open, opening files for reading rooted
// and relative to the directory d.
func (d Dir) Open(name string) (File, error) {
dir := string(d)
if dir == "" {
dir = "."
}
fullName := filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))
return os.Open(fullName)
}
// A FileSystem implements access to a collection of named files.
// The elements in a file path are separated by slash ('/', U+002F)
// characters, regardless of host operating system convention.
type FileSystem interface {
Open(name string) (File, error)
}
// A File is returned by a FileSystem's Open method and can be
// served by the FileServer implementation.
//
// The methods should behave the same as those on an *os.File.
type File interface {
io.Closer
io.Reader
Readdir(count int) ([]os.FileInfo, error)
Stat() (os.FileInfo, error)
}
func DirectoryListingHandler(path string, f File) Handler {
return HandlerFunc(func(w ResponseWriter, r *Request) {
files, err := f.Readdir(-1)
if err != nil {
log.Warn("DirectoryListingHandler: readdir failed", log.String("reason", err.Error()), log.String("path", r.URL.Path), log.String("url", r.URL.String()))
w.SetHeader(CodeTemporaryFailure, "readdir failed")
return
}
sort.Slice(files, func(i, j int) bool { return files[i].Name() < files[j].Name() })
w.SetHeader(CodeSuccess, DefaultMIMEType)
fmt.Fprintf(w, "# Index of %s\n\n", path)
fmt.Fprintln(w, "=> ../")
for _, ff := range files {
name := ff.Name()
if ff.IsDir() {
name += "/"
}
url := url.URL{Path: name}
fmt.Fprintf(w, "=> %v\n", url.String())
}
})
}
func FileContentHandler(name string, f File) Handler {
return HandlerFunc(func(w ResponseWriter, r *Request) {
mType := mime.TypeByExtension(path.Ext(name))
if mType == "" {
mType = DefaultMIMEType
}
w.SetHeader(CodeSuccess, mType)
if _, err := io.Copy(w, f); err != nil {
log.Error("FileContentHandler: failed to write file", err, log.String("fileName", name))
panic("error returning file contents")
}
})
}
func FileSystemHandler(fs FileSystem) Handler {
return HandlerFunc(func(w ResponseWriter, r *Request) {
if strings.Contains(r.URL.Path, "..") {
log.Warn("FileSystemHandler: possible directory traversal attack", log.String("path", r.URL.Path), log.String("url", r.URL.String()))
BadRequest(w, r)
return
}
if !strings.HasPrefix(r.URL.Path, "/") {
r.URL.Path = "/" + r.URL.Path
}
f, err := fs.Open(r.URL.Path)
if err != nil {
if os.IsNotExist(err) {
NotFoundHandler().ServeGemini(w, r)
return
}
log.Warn("FileSystemHandler: file open failed", log.String("reason", err.Error()), log.String("path", r.URL.Path), log.String("url", r.URL.String()))
w.SetHeader(CodeTemporaryFailure, "file open failed")
return
}
stat, err := f.Stat()
if err != nil {
log.Warn("FileSystemHandler: file stat failed", log.String("reason", err.Error()), log.String("path", r.URL.Path), log.String("url", r.URL.String()))
w.SetHeader(CodeTemporaryFailure, "file stat failed")
return
}
if stat.IsDir() {
// Look for index.gmi first before listing contents.
if !strings.HasSuffix(r.URL.Path, "/") {
RedirectPermanentHandler(r.URL.Path+"/").ServeGemini(w, r)
return
}
index, err := fs.Open(r.URL.Path + "index.gmi")
if errors.Is(err, os.ErrNotExist) {
DirectoryListingHandler(r.URL.Path, f).ServeGemini(w, r)
return
}
FileContentHandler("index.gmi", index).ServeGemini(w, r)
return
}
FileContentHandler(stat.Name(), f).ServeGemini(w, r)
})
}