Skip to content

Commit 8aae1c9

Browse files
committed
feat: add url prefix
Serve files under a subdirectory of url. This may be useful if the server is behind a reverse proxy and received the request without proxying path stripped. Add cmdline option `--prefix` to specify the url prefix. Add cmdline option `--base` to specify the case-insensitive url prefix.
1 parent be0f9b7 commit 8aae1c9

File tree

12 files changed

+240
-44
lines changed

12 files changed

+240
-44
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ ghfs [options]
111111
Use virtual empty directory as root directory.
112112
Useful to share alias directories only.
113113
114+
--prefix <path> ...
115+
Serve files under a specific sub url path.
116+
Could be useful if server is behind a reverse proxy and
117+
received the request without proxying path stripped.
118+
--base <path> ...
119+
Similar to --prefix, but the path is case-insensitive.
120+
114121
--default-sort <sortBy>
115122
Default sort rule for files and directories.
116123
Available sort key:

README.zh-CN.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ ghfs [选项]
110110
使用空的虚拟目录作为根目录。
111111
在仅需挂载别名的情况下较实用。
112112
113+
--prefix <path> ...
114+
在指定的URL子路径下提供服务。
115+
如果服务器在反向代理之后,且收到的请求并未去除代理路径前缀,可能较有用。
116+
--base <path> ...
117+
与--prefix类似,但路径不区分大小写。
118+
113119
--default-sort <排序规则>
114120
指定文件和目录的默认排序规则。
115121
可用的排序key:

src/param/cli.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ func init() {
2626
err = options.AddFlags("emptyroot", []string{"-R", "--empty-root"}, "GHFS_EMPTY_ROOT", "use virtual empty root directory")
2727
serverErrHandler.CheckFatal(err)
2828

29+
err = options.AddFlagValues("prefixurls", "--prefix", "", nil, "serve files under URL path instead of /")
30+
serverErrHandler.CheckFatal(err)
31+
32+
err = options.AddFlagValues("baseurls", "--base", "", nil, "serve files under case-insensitive URL path instead of /")
33+
serverErrHandler.CheckFatal(err)
34+
2935
opt = goNixArgParser.NewFlagValueOption("defaultsort", "--default-sort", "GHFS_DEFAULT_SORT", "/n", "default sort for files and directories")
3036
opt.Description = "Available sort key:\n- `n` sort by name ascending\n- `N` sort by name descending\n- `e` sort by type(suffix) ascending\n- `E` sort by type(suffix) descending\n- `s` sort by size ascending\n- `S` sort by size descending\n- `t` sort by modify time ascending\n- `T` sort by modify time descending\n- `_` no sort\nDirectory sort:\n- `/<key>` directories before files\n- `<key>/` directories after files\n- `<key>` directories mixed with files\n"
3137
err = options.Add(opt)
@@ -266,6 +272,14 @@ func doParseCli() []*Param {
266272
root, _ = util.NormalizeFsPath(root)
267273
param.Root = root
268274

275+
// normalize url prefixes
276+
prefixurls, _ := result.GetStrings("prefixurls")
277+
param.PrefixUrls = normalizeUrlPaths(prefixurls)
278+
279+
// normalize url bases
280+
baseurls, _ := result.GetStrings("baseurls")
281+
param.BaseUrls = normalizeUrlPaths(baseurls)
282+
269283
// dir indexes
270284
dirIndexes, _ := result.GetStrings("dirindexes")
271285
param.DirIndexes = normalizeFilenames(dirIndexes)

src/param/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ type Param struct {
1515
Root string
1616
EmptyRoot bool
1717

18+
PrefixUrls []string
19+
BaseUrls []string
20+
1821
DefaultSort string
1922
DirIndexes []string
2023
Aliases map[string]string

src/serverHandler/handler.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"strings"
1212
)
1313

14+
var defaultHandler = http.NotFoundHandler()
15+
1416
var createFileServer func(root string) http.Handler
1517

1618
type handler struct {

src/serverHandler/multiplexer.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import (
1010
"net/http"
1111
)
1212

13-
var defaultHandler = http.NotFoundHandler()
14-
1513
type aliasHandler struct {
1614
alias alias
1715
handler http.Handler
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package serverHandler
2+
3+
import (
4+
"../util"
5+
"net/http"
6+
)
7+
8+
type pathTransformer struct {
9+
prefixesAccurate []string
10+
prefixesNoCase []string
11+
nextHandler http.Handler
12+
}
13+
14+
func stripUrlPrefix(urlPathDir, urlPath, prefix string) string {
15+
if len(urlPath) == len(prefix) {
16+
return "/"
17+
} else if len(prefix) <= 1 {
18+
return urlPathDir
19+
} else {
20+
return urlPathDir[len(prefix):]
21+
}
22+
}
23+
24+
func (transformer pathTransformer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
25+
urlPath := util.CleanUrlPath(r.URL.Path)
26+
var urlPathDir string
27+
if len(urlPath) > 1 && r.URL.Path[len(r.URL.Path)-1] == '/' {
28+
urlPathDir = urlPath + "/"
29+
} else {
30+
urlPathDir = urlPath
31+
}
32+
r.URL.RawPath = urlPathDir
33+
34+
if len(transformer.prefixesAccurate) == 0 && len(transformer.prefixesNoCase) == 0 {
35+
r.URL.Path = urlPathDir
36+
transformer.nextHandler.ServeHTTP(w, r)
37+
return
38+
}
39+
40+
for _, prefix := range transformer.prefixesAccurate {
41+
if !util.HasUrlPrefixDir(urlPath, prefix) {
42+
continue
43+
}
44+
r.URL.Path = stripUrlPrefix(urlPathDir, urlPath, prefix)
45+
transformer.nextHandler.ServeHTTP(w, r)
46+
return
47+
}
48+
49+
for _, prefix := range transformer.prefixesNoCase {
50+
if !util.HasUrlPrefixDirNoCase(urlPath, prefix) {
51+
continue
52+
}
53+
r.URL.Path = stripUrlPrefix(urlPathDir, urlPath, prefix)
54+
transformer.nextHandler.ServeHTTP(w, r)
55+
return
56+
}
57+
58+
defaultHandler.ServeHTTP(w, r)
59+
}
60+
61+
func NewPathTransformer(prefixesAccurate, prefixesNoCase []string, handler http.Handler) http.Handler {
62+
return pathTransformer{prefixesAccurate, prefixesNoCase, handler}
63+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package serverHandler
2+
3+
import "testing"
4+
5+
func TestStripUrlPrefix(t *testing.T) {
6+
var result string
7+
8+
result = stripUrlPrefix("/", "/", "/")
9+
if result != "/" {
10+
t.Error(result)
11+
}
12+
13+
result = stripUrlPrefix("/foo", "/foo", "/")
14+
if result != "/foo" {
15+
t.Error(result)
16+
}
17+
18+
result = stripUrlPrefix("/foo/bar", "/foo/bar", "/")
19+
if result != "/foo/bar" {
20+
t.Error(result)
21+
}
22+
23+
result = stripUrlPrefix("/foo/bar/", "/foo/bar", "/")
24+
if result != "/foo/bar/" {
25+
t.Error(result)
26+
}
27+
28+
result = stripUrlPrefix("/foo", "/foo", "/foo")
29+
if result != "/" {
30+
t.Error(result)
31+
}
32+
33+
result = stripUrlPrefix("/foo/", "/foo", "/foo")
34+
if result != "/" {
35+
t.Error(result)
36+
}
37+
38+
result = stripUrlPrefix("/foo/bar", "/foo/bar", "/foo")
39+
if result != "/bar" {
40+
t.Error(result)
41+
}
42+
43+
result = stripUrlPrefix("/foo/bar/", "/foo/bar", "/foo")
44+
if result != "/bar/" {
45+
t.Error(result)
46+
}
47+
}

src/serverHandler/responseData.go

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@ type responseData struct {
4949
errors []error
5050
Status int
5151

52-
IsRoot bool
53-
Path string
54-
Paths []pathEntry
55-
RootRelPath string
52+
IsRoot bool
53+
Path string
54+
Paths []pathEntry
55+
RootRelPath string
56+
5657
File *os.File
5758
Item os.FileInfo
5859
ItemName string
@@ -71,34 +72,34 @@ func isSlash(c rune) bool {
7172
return c == '/'
7273
}
7374

74-
func getPathEntries(path string, tailSlash bool) []pathEntry {
75+
func getPathEntries(currDirRelPath, path string, tailSlash bool) []pathEntry {
7576
restPaths := strings.FieldsFunc(path, isSlash)
76-
paths := make([]string, 1, len(restPaths)+1)
77+
paths := make([]string, len(restPaths)+1)
7778
paths[0] = "/"
78-
paths = append(paths, restPaths...)
79+
copy(paths[1:], restPaths)
7980

80-
displayPathsCount := len(paths)
81+
pathFrags := len(paths)
8182

82-
pathsCount := displayPathsCount
83+
pathDepth := pathFrags
8384
if !tailSlash {
84-
pathsCount--
85+
pathDepth--
8586
}
8687

87-
pathEntries := make([]pathEntry, displayPathsCount)
88-
for i := 0; i < displayPathsCount; i++ {
89-
var rPath string
90-
switch {
91-
case i < pathsCount-1:
92-
rPath = strings.Repeat("../", pathsCount-1-i)
93-
case i == pathsCount-1:
94-
rPath = "./"
95-
default:
96-
rPath = "./" + strings.Join(paths[pathsCount:], "/") + "/"
88+
pathEntries := make([]pathEntry, pathFrags)
89+
for n := 1; n <= pathFrags; n++ {
90+
var relPath string
91+
if n < pathDepth {
92+
relPath = strings.Repeat("../", pathDepth-n)
93+
} else if n == pathDepth {
94+
relPath = currDirRelPath
95+
} else /*if n == pathDepth+1*/ {
96+
relPath = currDirRelPath + paths[pathDepth] + "/"
9797
}
9898

99+
i := n - 1
99100
pathEntries[i] = pathEntry{
100101
Name: paths[i],
101-
Path: rPath,
102+
Path: relPath,
102103
}
103104
}
104105

@@ -191,11 +192,19 @@ func (h *handler) mergeAlias(
191192
return subItems, aliasSubItems, errs
192193
}
193194

194-
func getSubItemPrefix(rawRequestPath string, tailSlash bool) string {
195-
if tailSlash {
195+
func getCurrDirRelPath(reqPath, rawReqPath string) string {
196+
if len(reqPath) == 1 && len(rawReqPath) > 1 && rawReqPath[len(rawReqPath)-1] != '/' {
197+
return "./" + path.Base(rawReqPath) + "/"
198+
} else {
196199
return "./"
200+
}
201+
}
202+
203+
func getSubItemPrefix(currDirRelPath, rawRequestPath string, tailSlash bool) string {
204+
if tailSlash {
205+
return currDirRelPath
197206
} else {
198-
return "./" + path.Base(rawRequestPath) + "/"
207+
return currDirRelPath + path.Base(rawRequestPath) + "/"
199208
}
200209
}
201210

@@ -266,13 +275,9 @@ func (h *handler) statIndexFile(rawReqPath, baseDir string, baseItem os.FileInfo
266275
func (h *handler) getResponseData(r *http.Request) *responseData {
267276
var errs []error
268277

269-
requestUri := r.URL.Path
270-
if len(requestUri) == 0 {
271-
requestUri = "/"
272-
}
273-
tailSlash := requestUri[len(requestUri)-1] == '/'
278+
rawReqPath := r.URL.Path
279+
tailSlash := rawReqPath[len(rawReqPath)-1] == '/'
274280

275-
rawReqPath := util.CleanUrlPath(requestUri)
276281
reqPath := util.CleanUrlPath(rawReqPath[len(h.urlPrefix):]) // strip url prefix path
277282
reqFsPath, _ := util.NormalizeFsPath(h.root + reqPath)
278283

@@ -311,12 +316,13 @@ func (h *handler) getResponseData(r *http.Request) *responseData {
311316
status := http.StatusOK
312317
isRoot := rawReqPath == "/"
313318

314-
pathEntries := getPathEntries(rawReqPath, tailSlash)
319+
currDirRelPath := getCurrDirRelPath(rawReqPath, r.URL.RawPath)
320+
pathEntries := getPathEntries(currDirRelPath, rawReqPath, tailSlash)
315321
var rootRelPath string
316322
if len(pathEntries) > 0 {
317323
rootRelPath = pathEntries[0].Path
318324
} else {
319-
rootRelPath = "./"
325+
rootRelPath = currDirRelPath
320326
}
321327

322328
file, item, _statErr := stat(reqFsPath, authSuccess && !h.emptyRoot)
@@ -356,11 +362,11 @@ func (h *handler) getResponseData(r *http.Request) *responseData {
356362
subItems = h.FilterItems(subItems)
357363
rawSortBy, sortState := sortInfos(subItems, rawQuery, h.defaultSort)
358364

359-
if h.emptyRoot && status == http.StatusOK && r.RequestURI != "/" {
365+
if h.emptyRoot && status == http.StatusOK && len(rawReqPath) > 1 {
360366
status = http.StatusNotFound
361367
}
362368

363-
subItemPrefix := getSubItemPrefix(rawReqPath, tailSlash)
369+
subItemPrefix := getSubItemPrefix(currDirRelPath, rawReqPath, tailSlash)
364370

365371
canUpload := h.getCanUpload(item, rawReqPath, reqFsPath)
366372
canMkdir := h.getCanMkdir(item, rawReqPath, reqFsPath)
@@ -400,10 +406,11 @@ func (h *handler) getResponseData(r *http.Request) *responseData {
400406
errors: errs,
401407
Status: status,
402408

403-
IsRoot: isRoot,
404-
Path: rawReqPath,
405-
Paths: pathEntries,
406-
RootRelPath: rootRelPath,
409+
IsRoot: isRoot,
410+
Path: rawReqPath,
411+
Paths: pathEntries,
412+
RootRelPath: rootRelPath,
413+
407414
File: file,
408415
Item: item,
409416
ItemName: itemName,

src/serverHandler/responseData_test.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ import "testing"
55
func TestGetPathEntries(t *testing.T) {
66
var result []pathEntry
77

8-
result = getPathEntries("/a/b/c", false)
8+
result = getPathEntries("./", "/", true)
9+
if len(result) != 1 {
10+
t.Error(len(result))
11+
}
12+
if result[0].Path != "./" || result[0].Name != "/" {
13+
t.Error(result[0])
14+
}
15+
16+
result = getPathEntries("./", "/a/b/c", false)
917
if len(result) != 4 {
1018
t.Error(len(result))
1119
}
@@ -22,7 +30,7 @@ func TestGetPathEntries(t *testing.T) {
2230
t.Error(result[3])
2331
}
2432

25-
result = getPathEntries("/a/b/c", true)
33+
result = getPathEntries("./", "/a/b/c", true)
2634
if len(result) != 4 {
2735
t.Error(len(result))
2836
}
@@ -38,4 +46,12 @@ func TestGetPathEntries(t *testing.T) {
3846
if result[3].Path != "./" || result[3].Name != "c" {
3947
t.Error(result[3])
4048
}
49+
50+
result = getPathEntries("./foo", "/", true)
51+
if len(result) != 1 {
52+
t.Error(len(result))
53+
}
54+
if result[0].Path != "./foo" || result[0].Name != "/" {
55+
t.Error(result[0])
56+
}
4157
}

0 commit comments

Comments
 (0)