aboutsummaryrefslogtreecommitdiff
path: root/fs.go
blob: 2f931cfb9f408849173064f978c356177fa26eb3 (plain) (blame)
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
package gemini

import (
	"io"
	"io/fs"
	"mime"
	"path"
)

func init() {
	// Add Gemini mime types
	mime.AddExtensionType(".gmi", "text/gemini")
	mime.AddExtensionType(".gemini", "text/gemini")
}

// FileServer returns a handler that serves Gemini requests with the contents
// of the provided file system.
//
// To use the operating system's file system implementation, use os.DirFS:
//
//     gemini.FileServer(os.DirFS("/tmp"))
func FileServer(fsys fs.FS) Handler {
	return fileServer{fsys}
}

type fileServer struct {
	fs.FS
}

func (fs fileServer) ServeGemini(w ResponseWriter, r *Request) {
	ServeFile(w, fs, r.URL.Path)
}

// ServeFile responds to the request with the contents of the named file
// or directory.
//
// If the provided file or directory name is a relative path, it is interpreted
// relative to the current directory and may ascend to parent directories. If
// the provided name is constructed from user input, it should be sanitized
// before calling ServeFile.
func ServeFile(w ResponseWriter, fsys fs.FS, name string) {
	f, err := openFile(fsys, name)
	if err != nil {
		w.Status(StatusNotFound)
		return
	}
	// Detect mimetype
	ext := path.Ext(name)
	mimetype := mime.TypeByExtension(ext)
	w.Meta(mimetype)
	// Copy file to response writer
	_, _ = io.Copy(w, f)
}

func openFile(fsys fs.FS, name string) (fs.File, error) {
	if name == "/" {
		name = "."
	} else {
		name = strings.TrimPrefix(name, "/")
	}

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

	stat, err := f.Stat()
	if err != nil {
		return nil, err
	}
	if stat.Mode().IsRegular() {
		return f, nil
	}

	if stat.IsDir() {
		// Try opening index.gmi
		f, err := fsys.Open(path.Join(name, "index.gmi"))
		if err != nil {
			return nil, err
		}
		stat, err := f.Stat()
		if err != nil {
			return nil, err
		}
		if stat.Mode().IsRegular() {
			return f, nil
		}
	}

	return nil, fs.ErrNotExist
}