- commit
- 1b8771c
- parent
- 8186562
- author
- Eric Bower
- date
- 2024-03-02 19:48:07 +0000 UTC
fix(pgs): various redirect fixes (#92) Closes: #91
5 files changed,
+237,
-93
+3,
-82
1@@ -20,7 +20,6 @@ import (
2 "github.com/picosh/pico/shared"
3 "github.com/picosh/pico/shared/storage"
4 sst "github.com/picosh/pobj/storage"
5- "github.com/picosh/send/send/utils"
6 )
7
8 type AssetHandler struct {
9@@ -151,85 +150,6 @@ func createRssHandler(by string) http.HandlerFunc {
10 }
11 }
12
13-type HttpReply struct {
14- Filepath string
15- Query map[string]string
16- Status int
17-}
18-
19-func calcPossibleRoutes(projectName, fp string, userRedirects []*RedirectRule) []*HttpReply {
20- fname := filepath.Base(fp)
21- fdir := filepath.Dir(fp)
22- fext := filepath.Ext(fp)
23- mimeType := storage.GetMimeType(fp)
24- rts := []*HttpReply{}
25- notFound := &HttpReply{
26- Filepath: filepath.Join(projectName, "404.html"),
27- Status: 404,
28- }
29-
30- for _, redirect := range userRedirects {
31- rr := regexp.MustCompile(redirect.From)
32- match := rr.FindStringSubmatch(fp)
33- if len(match) > 0 {
34- ruleRoute := shared.GetAssetFileName(&utils.FileEntry{
35- Filepath: filepath.Join(projectName, redirect.To),
36- })
37- rule := &HttpReply{
38- Filepath: ruleRoute,
39- Status: redirect.Status,
40- Query: redirect.Query,
41- }
42- if redirect.Force {
43- rts = append([]*HttpReply{rule}, rts...)
44- } else {
45- rts = append(rts, rule)
46- }
47- }
48- }
49-
50- // user routes take precedence
51- if len(rts) > 0 {
52- rts = append(rts, notFound)
53- return rts
54- }
55-
56- // file extension is unknown
57- if mimeType == "text/plain" && fext != ".txt" {
58- dirRoute := shared.GetAssetFileName(&utils.FileEntry{
59- Filepath: filepath.Join(projectName, fp, "index.html"),
60- })
61- // we need to accommodate routes that are just directories
62- // and point the user to the index.html of each root dir.
63- nameRoute := shared.GetAssetFileName(&utils.FileEntry{
64- Filepath: filepath.Join(
65- projectName,
66- fdir,
67- fmt.Sprintf("%s.html", fname),
68- ),
69- })
70- rts = append(rts,
71- &HttpReply{Filepath: nameRoute, Status: 200},
72- &HttpReply{Filepath: dirRoute, Status: 200},
73- notFound,
74- )
75- return rts
76- }
77-
78- defRoute := shared.GetAssetFileName(&utils.FileEntry{
79- Filepath: filepath.Join(projectName, fdir, fname),
80- })
81-
82- rts = append(rts,
83- &HttpReply{
84- Filepath: defRoute, Status: 200,
85- },
86- notFound,
87- )
88-
89- return rts
90-}
91-
92 func (h *AssetHandler) handle(w http.ResponseWriter) {
93 var redirects []*RedirectRule
94 redirectFp, _, _, err := h.Storage.GetObject(h.Bucket, filepath.Join(h.ProjectDir, "_redirects"))
95@@ -249,7 +169,8 @@ func (h *AssetHandler) handle(w http.ResponseWriter) {
96 }
97 }
98
99- routes := calcPossibleRoutes(h.ProjectDir, h.Filepath, redirects)
100+ routes := calcRoutes(h.ProjectDir, h.Filepath, redirects)
101+
102 var contents io.ReadCloser
103 contentType := ""
104 assetFilepath := ""
105@@ -470,7 +391,7 @@ func createSubdomainRoutes(hasPerm HasPerm) []shared.Route {
106 return []shared.Route{
107 shared.NewRoute("GET", "/", assetRequest),
108 shared.NewRoute("GET", "(/.+.(?:jpg|jpeg|png|gif|webp|svg))(/.+)", imgRequest),
109- shared.NewRoute("GET", "/(.+)", assetRequest),
110+ shared.NewRoute("GET", "(/.+)", assetRequest),
111 }
112 }
113
+123,
-0
1@@ -0,0 +1,123 @@
2+package pgs
3+
4+import (
5+ "fmt"
6+ "net/http"
7+ "path/filepath"
8+ "regexp"
9+
10+ "github.com/picosh/pico/shared"
11+ "github.com/picosh/pico/shared/storage"
12+ "github.com/picosh/send/send/utils"
13+)
14+
15+type HttpReply struct {
16+ Filepath string
17+ Query map[string]string
18+ Status int
19+}
20+
21+func expandRoute(projectName, fp string, status int) []*HttpReply {
22+ mimeType := storage.GetMimeType(fp)
23+ fname := filepath.Base(fp)
24+ fdir := filepath.Dir(fp)
25+ fext := filepath.Ext(fp)
26+ routes := []*HttpReply{}
27+
28+ if mimeType != "text/plain" {
29+ return routes
30+ }
31+
32+ if fext == ".txt" {
33+ return routes
34+ }
35+
36+ if fname != "" && fname != "/" {
37+ // we need to accommodate routes that are just directories
38+ // and point the user to the index.html of each root dir.
39+ nameRoute := shared.GetAssetFileName(&utils.FileEntry{
40+ Filepath: filepath.Join(
41+ projectName,
42+ fdir,
43+ fmt.Sprintf("%s.html", fname),
44+ ),
45+ })
46+
47+ routes = append(
48+ routes,
49+ &HttpReply{Filepath: nameRoute, Status: status},
50+ )
51+ }
52+
53+ dirRoute := shared.GetAssetFileName(&utils.FileEntry{
54+ Filepath: filepath.Join(projectName, fp, "index.html"),
55+ })
56+
57+ routes = append(
58+ routes,
59+ &HttpReply{Filepath: dirRoute, Status: status},
60+ )
61+
62+ return routes
63+}
64+
65+func calcRoutes(projectName, fp string, userRedirects []*RedirectRule) []*HttpReply {
66+ notFound := &HttpReply{
67+ Filepath: filepath.Join(projectName, "404.html"),
68+ Status: 404,
69+ }
70+
71+ rts := expandRoute(projectName, fp, http.StatusOK)
72+
73+ fext := filepath.Ext(fp)
74+ // add route as-is without expansion if there is a file ext
75+ if fp != "" && fext != "" {
76+ defRoute := shared.GetAssetFileName(&utils.FileEntry{
77+ Filepath: filepath.Join(projectName, fp),
78+ })
79+
80+ rts = append(rts,
81+ &HttpReply{
82+ Filepath: defRoute, Status: 200,
83+ },
84+ )
85+ }
86+
87+ // user routes
88+ for _, redirect := range userRedirects {
89+ rr := regexp.MustCompile(redirect.From)
90+ match := rr.FindStringSubmatch(fp)
91+ if len(match) > 0 {
92+ userReply := []*HttpReply{}
93+ ruleRoute := shared.GetAssetFileName(&utils.FileEntry{
94+ Filepath: filepath.Join(projectName, redirect.To),
95+ })
96+ var rule *HttpReply
97+ if redirect.To != "" && redirect.To != "/" {
98+ rule = &HttpReply{
99+ Filepath: ruleRoute,
100+ Status: redirect.Status,
101+ Query: redirect.Query,
102+ }
103+ userReply = append(userReply, rule)
104+ }
105+
106+ expandedRoutes := expandRoute(projectName, redirect.To, redirect.Status)
107+ userReply = append(userReply, expandedRoutes...)
108+
109+ if redirect.Force {
110+ rts = userReply
111+ } else {
112+ rts = append(rts, userReply...)
113+ }
114+ // quit after first match
115+ break
116+ }
117+ }
118+
119+ rts = append(rts,
120+ notFound,
121+ )
122+
123+ return rts
124+}
+70,
-9
1@@ -13,11 +13,11 @@ type RouteFixture struct {
2 Expected []*HttpReply
3 }
4
5-func TestCalcPossibleRoutes(t *testing.T) {
6+func TestCalcRoutes(t *testing.T) {
7 fixtures := []RouteFixture{
8 {
9 Name: "basic-index",
10- Actual: calcPossibleRoutes("test", "index.html", []*RedirectRule{}),
11+ Actual: calcRoutes("test", "/index.html", []*RedirectRule{}),
12 Expected: []*HttpReply{
13 {Filepath: "test/index.html", Status: 200},
14 {Filepath: "test/404.html", Status: 404},
15@@ -25,7 +25,7 @@ func TestCalcPossibleRoutes(t *testing.T) {
16 },
17 {
18 Name: "basic-txt",
19- Actual: calcPossibleRoutes("test", "index.txt", []*RedirectRule{}),
20+ Actual: calcRoutes("test", "/index.txt", []*RedirectRule{}),
21 Expected: []*HttpReply{
22 {Filepath: "test/index.txt", Status: 200},
23 {Filepath: "test/404.html", Status: 404},
24@@ -33,7 +33,7 @@ func TestCalcPossibleRoutes(t *testing.T) {
25 },
26 {
27 Name: "basic-named",
28- Actual: calcPossibleRoutes("test", "wow.html", []*RedirectRule{}),
29+ Actual: calcRoutes("test", "/wow.html", []*RedirectRule{}),
30 Expected: []*HttpReply{
31 {Filepath: "test/wow.html", Status: 200},
32 {Filepath: "test/404.html", Status: 404},
33@@ -41,7 +41,7 @@ func TestCalcPossibleRoutes(t *testing.T) {
34 },
35 {
36 Name: "subdirectory-index",
37- Actual: calcPossibleRoutes("test", "nice/index.html", []*RedirectRule{}),
38+ Actual: calcRoutes("test", "/nice/index.html", []*RedirectRule{}),
39 Expected: []*HttpReply{
40 {Filepath: "test/nice/index.html", Status: 200},
41 {Filepath: "test/404.html", Status: 404},
42@@ -49,7 +49,7 @@ func TestCalcPossibleRoutes(t *testing.T) {
43 },
44 {
45 Name: "subdirectory-named",
46- Actual: calcPossibleRoutes("test", "nice/wow.html", []*RedirectRule{}),
47+ Actual: calcRoutes("test", "/nice/wow.html", []*RedirectRule{}),
48 Expected: []*HttpReply{
49 {Filepath: "test/nice/wow.html", Status: 200},
50 {Filepath: "test/404.html", Status: 404},
51@@ -57,7 +57,7 @@ func TestCalcPossibleRoutes(t *testing.T) {
52 },
53 {
54 Name: "subdirectory-bare",
55- Actual: calcPossibleRoutes("test", "nice", []*RedirectRule{}),
56+ Actual: calcRoutes("test", "/nice", []*RedirectRule{}),
57 Expected: []*HttpReply{
58 {Filepath: "test/nice.html", Status: 200},
59 {Filepath: "test/nice/index.html", Status: 200},
60@@ -66,7 +66,7 @@ func TestCalcPossibleRoutes(t *testing.T) {
61 },
62 {
63 Name: "spa",
64- Actual: calcPossibleRoutes("test", "nice", []*RedirectRule{
65+ Actual: calcRoutes("test", "/nice", []*RedirectRule{
66 {
67 From: "/*",
68 To: "/index.html",
69@@ -74,18 +74,79 @@ func TestCalcPossibleRoutes(t *testing.T) {
70 },
71 }),
72 Expected: []*HttpReply{
73+ {Filepath: "test/nice.html", Status: 200},
74+ {Filepath: "test/nice/index.html", Status: 200},
75 {Filepath: "test/index.html", Status: 200},
76 {Filepath: "test/404.html", Status: 404},
77 },
78 },
79 {
80 Name: "xml",
81- Actual: calcPossibleRoutes("test", "index.xml", []*RedirectRule{}),
82+ Actual: calcRoutes("test", "/index.xml", []*RedirectRule{}),
83 Expected: []*HttpReply{
84 {Filepath: "test/index.xml", Status: 200},
85 {Filepath: "test/404.html", Status: 404},
86 },
87 },
88+ {
89+ Name: "redirectRule",
90+ Actual: calcRoutes(
91+ "test",
92+ "/wow",
93+ []*RedirectRule{
94+ {
95+ From: "/wow",
96+ To: "index.html",
97+ Status: 301,
98+ },
99+ },
100+ ),
101+ Expected: []*HttpReply{
102+ {Filepath: "test/wow.html", Status: 200},
103+ {Filepath: "test/wow/index.html", Status: 200},
104+ {Filepath: "test/index.html", Status: 301},
105+ {Filepath: "test/404.html", Status: 404},
106+ },
107+ },
108+ {
109+ Name: "root",
110+ Actual: calcRoutes(
111+ "test",
112+ "/wow",
113+ []*RedirectRule{
114+ {
115+ From: "/wow",
116+ To: "/",
117+ Status: 301,
118+ },
119+ },
120+ ),
121+ Expected: []*HttpReply{
122+ {Filepath: "test/wow.html", Status: 200},
123+ {Filepath: "test/wow/index.html", Status: 200},
124+ {Filepath: "test/index.html", Status: 301},
125+ {Filepath: "test/404.html", Status: 404},
126+ },
127+ },
128+ {
129+ Name: "force",
130+ Actual: calcRoutes(
131+ "test",
132+ "/wow",
133+ []*RedirectRule{
134+ {
135+ From: "/wow",
136+ To: "/",
137+ Status: 301,
138+ Force: true,
139+ },
140+ },
141+ ),
142+ Expected: []*HttpReply{
143+ {Filepath: "test/index.html", Status: 301},
144+ {Filepath: "test/404.html", Status: 404},
145+ },
146+ },
147 }
148
149 for _, fixture := range fixtures {
+11,
-2
1@@ -2,6 +2,7 @@ package pgs
2
3 import (
4 "fmt"
5+ "net/http"
6 "regexp"
7 "strconv"
8 "strings"
9@@ -120,7 +121,15 @@ func parseRedirectText(text string) ([]*RedirectRule, error) {
10 queryParts := parts[:toIndex]
11 to := parts[toIndex]
12 lastParts := parts[toIndex+1:]
13- sts, frcd := hasStatusCode(lastParts[0])
14+ conditions := map[string]string{}
15+ sts := http.StatusOK
16+ frcd := false
17+ if len(lastParts) > 0 {
18+ sts, frcd = hasStatusCode(lastParts[0])
19+ }
20+ if len(lastParts) > 1 {
21+ conditions = parsePairs(lastParts[1:])
22+ }
23
24 rules = append(rules, &RedirectRule{
25 To: to,
26@@ -128,7 +137,7 @@ func parseRedirectText(text string) ([]*RedirectRule, error) {
27 Status: sts,
28 Force: frcd,
29 Query: parsePairs(queryParts),
30- Conditions: parsePairs(lastParts[1:]),
31+ Conditions: conditions,
32 })
33 }
34 }
+30,
-0
1@@ -28,8 +28,38 @@ func TestParseRedirectText(t *testing.T) {
2 },
3 }
4
5+ withStatus := RedirectFixture{
6+ name: "with-status",
7+ input: "/wow /index.html 301",
8+ expect: []*RedirectRule{
9+ {
10+ From: "/wow",
11+ To: "/index.html",
12+ Status: 301,
13+ Query: empty,
14+ Conditions: empty,
15+ },
16+ },
17+ }
18+
19+ noStatus := RedirectFixture{
20+ name: "no-status",
21+ input: "/wow /index.html",
22+ expect: []*RedirectRule{
23+ {
24+ From: "/wow",
25+ To: "/index.html",
26+ Status: 200,
27+ Query: empty,
28+ Conditions: empty,
29+ },
30+ },
31+ }
32+
33 fixtures := []RedirectFixture{
34 spa,
35+ withStatus,
36+ noStatus,
37 }
38
39 for _, fixture := range fixtures {