git » gofer » next » tree

[next] / server / dir.go

package server

import (
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"blitiri.com.ar/go/gofer/config"
)

type FileSystem struct {
	fs http.FileSystem

	// On-disk root of the served directory. Used by the PUT/DELETE handlers
	// to resolve request paths to filesystem paths. Empty if the underlying
	// http.FileSystem is not backed by a local directory.
	root string

	opts config.DirOpts
}

func NewFS(root string, fs http.FileSystem, opts config.DirOpts) *FileSystem {
	return &FileSystem{
		fs:   fs,
		root: root,
		opts: opts,
	}
}

// prefixMatch returns the value associated with the longest prefix of name in
// m, or false if no prefix matches.
func prefixMatch(m map[string]bool, name string) bool {
	if name == "" {
		name = "/"
	}
	name = filepath.Clean(name)

	longestP := ""
	value := false
	for p, val := range m {
		p = filepath.Clean(p)
		if strings.HasPrefix(name, p) && len(p) > len(longestP) {
			longestP = p
			value = val
		}
	}

	return value
}

func ListingEnabled(opts *config.DirOpts, name string) bool {
	return prefixMatch(opts.Listing, name)
}

func PutEnabled(opts *config.DirOpts, name string) bool {
	return prefixMatch(opts.Put, name)
}

func DeleteEnabled(opts *config.DirOpts, name string) bool {
	return prefixMatch(opts.Delete, name)
}

// localPath resolves a URL path to an absolute on-disk path under fs.root.
// It returns an error if writes are not supported (fs.root is unset) or if
// the resulting path would escape the root.
func (fs *FileSystem) localPath(name string) (string, error) {
	if fs.root == "" {
		return "", os.ErrPermission
	}
	absRoot, err := filepath.Abs(fs.root)
	if err != nil {
		return "", err
	}
	abs := filepath.Join(absRoot, filepath.FromSlash(name))
	rel, err := filepath.Rel(absRoot, abs)
	if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
		return "", os.ErrPermission
	}
	return abs, nil
}

func (fs *FileSystem) excluded(name string) bool {
	for _, re := range fs.opts.Exclude {
		if re.MatchString(name) {
			return true
		}
	}
	return false
}

func (fs *FileSystem) Open(name string) (http.File, error) {
	if fs.excluded(name) {
		return nil, os.ErrNotExist
	}

	f, err := fs.fs.Open(name)
	if err != nil {
		return nil, err
	}

	// If it's not a directory, let it be.
	if s, _ := f.Stat(); s == nil || !s.IsDir() {
		return f, nil
	}

	f = wrappedFile{File: f, name: name, opts: &fs.opts}

	if ListingEnabled(&fs.opts, name) {
		return f, nil
	}

	// It's a directory, and listing not allowed.
	// However, if there is an index.html, we let it be served.
	index := filepath.Join(name, "index.html")
	if idxf, err := fs.fs.Open(index); err == nil {
		idxf.Close()
		return f, err
	}

	f.Close()
	return nil, os.ErrNotExist
}

type wrappedFile struct {
	http.File
	name string
	opts *config.DirOpts
}

func (f wrappedFile) Readdir(count int) ([]os.FileInfo, error) {
	if !ListingEnabled(f.opts, f.name) {
		return nil, os.ErrNotExist
	}

	// Exclude files from listings too.
	all, err := f.File.Readdir(count)
	var fis []os.FileInfo
outer:
	for _, fi := range all {
		for _, re := range f.opts.Exclude {
			name := filepath.Join(f.name, fi.Name())
			if re.MatchString(name) {
				continue outer
			}
		}
		fis = append(fis, fi)
	}
	return fis, err
}