summaryrefslogtreecommitdiff
path: root/contrib/fs/dir.go
blob: 56598043200afba73c4fbed080d6e41909f1bb89 (plain)
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package fs

import (
	"bytes"
	"io"
	"io/fs"
	"sort"
	"strings"
	"text/template"

	"tildegit.org/tjp/gus"
)

// ResolveDirectory opens the directory corresponding to a request path.
//
// The string is the full path to the directory. If the returned ReadDirFile
// is not nil, it will be open and must be closed by the caller.
func ResolveDirectory(
	request *gus.Request,
	fileSystem fs.FS,
) (string, fs.ReadDirFile, error) {
	path := strings.Trim(request.Path, "/")
	if path == "" {
		path = "."
	}

	file, err := fileSystem.Open(path)
	if isNotFound(err) {
		return "", nil, nil
	}
	if err != nil {
		return "", nil, err
	}

	isDir, err := fileIsDir(file)
	if err != nil {
		_ = file.Close()
		return "", nil, err
	}

	if !isDir {
		_ = file.Close()
		return "", nil, nil
	}

	dirFile, ok := file.(fs.ReadDirFile)
	if !ok {
		_ = file.Close()
		return "", nil, nil
	}

	return path, dirFile, nil
}

// ResolveDirectoryDefault finds any of the provided filenames within a directory.
//
// If it does not find any of the filenames it returns "", nil, nil.
func ResolveDirectoryDefault(
	fileSystem fs.FS,
	dirPath string,
	dir fs.ReadDirFile,
	filenames []string,
) (string, fs.File, error) {
	entries, err := dir.ReadDir(0)
	if err != nil {
		return "", nil, err
	}
	sort.Slice(entries, func(a, b int) bool {
		return entries[a].Name() < entries[b].Name()
	})

	for _, filename := range filenames {
		idx := sort.Search(len(entries), func(i int) bool {
			return entries[i].Name() <= filename
		})

		if idx < len(entries) && entries[idx].Name() == filename {
			path := dirPath + "/" + filename
			file, err := fileSystem.Open(path)
			return path, file, err
		}
	}

	return "", nil, nil
}

// RenderDirectoryListing provides an io.Reader with the output of a directory listing template.
//
// The template is provided the following namespace:
//   - .FullPath: the complete path to the listed directory
//   - .DirName: the name of the directory itself
//   - .Entries: the []fs.DirEntry of the directory contents
//   - .Hostname: the hostname of the server hosting the file
//   - .Port: the port on which the server is listening
//
// Each entry in .Entries has the following fields:
//   - .Name the string name of the item within the directory
//   - .IsDir is a boolean
//   - .Type is the FileMode bits
//   - .Info is a method returning (fs.FileInfo, error)
func RenderDirectoryListing(
	path string,
	dir fs.ReadDirFile,
	template *template.Template,
	server gus.Server,
) (io.Reader, error) {
	buf := &bytes.Buffer{}

	environ, err := dirlistNamespace(path, dir, server)
	if err != nil {
		return nil, err
	}

	if err := template.Execute(buf, environ); err != nil {
		return nil, err
	}

	return buf, nil
}

func dirlistNamespace(path string, dirFile fs.ReadDirFile, server gus.Server) (map[string]any, error) {
	entries, err := dirFile.ReadDir(0)
	if err != nil {
		return nil, err
	}

	sort.Slice(entries, func(i, j int) bool {
		return entries[i].Name() < entries[j].Name()
	})

	var dirname string
	if path == "." {
		dirname = "(root)"
	} else {
		dirname = path[strings.LastIndex(path, "/")+1:]
	}

	hostname := "none"
	port := "0"
	if server != nil {
		hostname = server.Hostname()
		port = server.Port()
	}

	m := map[string]any{
		"FullPath": path,
		"DirName":  dirname,
		"Entries":  entries,
		"Hostname": hostname,
		"Port":     port,
	}

	return m, nil
}