repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

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
M pgs/api.go
+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 
A pgs/cal_route.go
+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+}
M pgs/calc_route_test.go
+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 {
M pgs/redirect.go
+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 	}
M pgs/redirect_test.go
+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 {