Skip to content

Commit

Permalink
Added Static File Handler (#674)
Browse files Browse the repository at this point in the history
  • Loading branch information
KedarisettiSreeVamsi authored Jun 26, 2024
1 parent f10b5c5 commit cd5b92e
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 3 deletions.
75 changes: 75 additions & 0 deletions docs/advanced-guide/serving-static-files/page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Serving Static Files using GoFr

Often, we are required to serve static content such as a default profile image, a favicon, or a background image for our
web application. We want to have a mechanism to serve that static content without the hassle of implementing it from scratch.

GoFr provides a default mechanism where if a `static` folder is available in the directory of the application,
it automatically provides an endpoint with `/static/<filename>`, here filename refers to the file we want to get static content to be served.

Example project structure:

```dotenv
project_folder
|
|---configs
| .env
|---static
| img1.jpeg
| img2.png
| img3.jpeg
| main.go
| main_test.go
```

main.go code:

```go
package main

import "gofr.dev/pkg/gofr"

func main(){
app := gofr.New()
app.Run()
}
```

Additionally, if we want to serve more static endpoints, we have a dedicated function called `AddStaticFiles()`
which takes 2 parameters `endpoint` and the `filepath` of the static folder which we want to serve.

Example project structure:

```dotenv
project_folder
|
|---configs
| .env
|---static
| img1.jpeg
| img2.png
| img3.jpeg
|---public
| |---css
| | main.css
| |---js
| | main.js
| | index.html
| main.go
| main_test.go
```

main.go file:

```go
package main

import "gofr.dev/pkg/gofr"

func main(){
app := gofr.New()
app.AddStaticFiles("public", "./public")
app.Run()
}
```

In the above example, both endpoints `/public` and `/static` are available for the app to render the static content.
30 changes: 30 additions & 0 deletions pkg/gofr/gofr.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
Expand All @@ -30,6 +31,8 @@ import (
"gofr.dev/pkg/gofr/service"
)

const defaultPublicStaticDir = "static"

// App is the main application in the GoFr framework.
type App struct {
// Config can be used by applications to fetch custom configurations from environment or file.
Expand Down Expand Up @@ -92,6 +95,14 @@ func New() *App {

app.subscriptionManager = newSubscriptionManager(app.container)

// static fileserver
currentWd, _ := os.Getwd()
checkDirectory := filepath.Join(currentWd, defaultPublicStaticDir)

if _, err = os.Stat(checkDirectory); err == nil {
app.AddStaticFiles(defaultPublicStaticDir, checkDirectory)
}

return app
}

Expand Down Expand Up @@ -439,3 +450,22 @@ func contains(elems []string, v string) bool {

return false
}

func (a *App) AddStaticFiles(endpoint, filePath string) {
a.httpRegistered = true

// update file path based on current directory if it starts with ./
if strings.HasPrefix(filePath, "./") {
currentWorkingDir, _ := os.Getwd()
filePath = filepath.Join(currentWorkingDir, filePath)
}

endpoint = "/" + strings.TrimPrefix(endpoint, "/")

if _, err := os.Stat(filePath); err != nil {
a.container.Logger.Errorf("error in registering '%s' static endpoint, error: %v", endpoint, err)
return
}

a.httpServer.router.AddStaticFiles(endpoint, filePath)
}
138 changes: 138 additions & 0 deletions pkg/gofr/gofr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -561,3 +561,141 @@ func Test_AddCronJob_Success(t *testing.T) {

assert.Truef(t, pass, "unable to add cron job to cron table")
}

func TestStaticHandler(t *testing.T) {
const indexHTML = "indexTest.html"

// Generating some files for testing
htmlContent := []byte("<html><head><title>Test Static File</title></head><body><p>Testing Static File</p></body></html>")

createPublicDirectory(t, defaultPublicStaticDir, htmlContent)

defer os.Remove("static/indexTest.html")

createPublicDirectory(t, "testdir", htmlContent)

defer os.RemoveAll("testdir")

app := New()

app.AddStaticFiles("gofrTest", "./testdir")

app.httpRegistered = true
app.httpServer.port = 8022

go app.Run()
time.Sleep(1 * time.Second)

host := "http://localhost:8022"

tests := []struct {
desc string
method string
path string
statusCode int
expectedBody string
expectedBodyLength int
expectedResponseHeaderType string
}{
{
desc: "check file content index.html", method: http.MethodGet, path: "/" + defaultPublicStaticDir + "/" + indexHTML,
statusCode: http.StatusOK, expectedBodyLength: len(htmlContent),
expectedResponseHeaderType: "text/html; charset=utf-8", expectedBody: string(htmlContent),
},
{
desc: "check public endpoint", method: http.MethodGet,
path: "/" + defaultPublicStaticDir, statusCode: http.StatusNotFound,
},
{
desc: "check file content index.html in custom dir", method: http.MethodGet, path: "/" + "gofrTest" + "/" + indexHTML,
statusCode: http.StatusOK, expectedBodyLength: len(htmlContent),
expectedResponseHeaderType: "text/html; charset=utf-8", expectedBody: string(htmlContent),
},
{
desc: "check public endpoint in custom dir", method: http.MethodGet, path: "/" + "gofrTest",
statusCode: http.StatusNotFound,
},
}

for i, tc := range tests {
request, err := http.NewRequestWithContext(context.Background(), tc.method, host+tc.path, http.NoBody)
if err != nil {
t.Fatalf("TEST[%d], Failed to create request, error: %s", i, err)
}

request.Header.Set("Content-Type", "application/json")

client := http.Client{}

resp, err := client.Do(request)
if err != nil {
t.Fatalf("TEST[%d], Request failed, error: %s", i, err)
}

bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("TEST[%d], Failed to read response body, error: %s", i, err)
}

body := string(bodyBytes)

assert.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.statusCode, resp.StatusCode, "TEST[%d], Failed with Status Body.\n%s", i, tc.desc)

if tc.expectedBody != "" {
assert.Contains(t, body, tc.expectedBody, "TEST[%d], Failed with Expected Body.\n%s", i, tc.desc)
}

if tc.expectedBodyLength != 0 {
contentLength := resp.Header.Get("Content-Length")
assert.Equal(t, strconv.Itoa(tc.expectedBodyLength), contentLength, "TEST[%d], Failed at Content-Length.\n%s", i, tc.desc)
}

if tc.expectedResponseHeaderType != "" {
assert.Equal(t,
tc.expectedResponseHeaderType,
resp.Header.Get("Content-Type"),
"TEST[%d], Failed at Expected Content-Type.\n%s", i, tc.desc)
}

resp.Body.Close()
}
}

func TestStaticHandlerInvalidFilePath(t *testing.T) {
// Generating some files for testing
logs := testutil.StderrOutputForFunc(func() {
app := New()

app.AddStaticFiles("gofrTest", ".//,.!@#$%^&")
})

assert.Contains(t, logs, "no such file or directory")
assert.Contains(t, logs, "error in registering '/gofrTest' static endpoint")
}

func createPublicDirectory(t *testing.T, defaultPublicStaticDir string, htmlContent []byte) {
t.Helper()

const indexHTML = "indexTest.html"

directory := "./" + defaultPublicStaticDir
if _, err := os.Stat(directory); err != nil {
if err = os.Mkdir("./"+defaultPublicStaticDir, os.ModePerm); err != nil {
t.Fatalf("Couldn't create a "+defaultPublicStaticDir+" directory, error: %s", err)
}
}

file, err := os.Create(filepath.Join(directory, indexHTML))

if err != nil {
t.Fatalf("Couldn't create %s file", indexHTML)
}

_, err = file.Write(htmlContent)
if err != nil {
t.Fatalf("Couldn't write to %s file", indexHTML)
}

file.Close()
}
39 changes: 37 additions & 2 deletions pkg/gofr/http/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package http

import (
"net/http"

"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"os"
"path/filepath"
"strings"

"github.com/gorilla/mux"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

// Router is responsible for routing HTTP request.
Expand Down Expand Up @@ -45,3 +47,36 @@ func (rou *Router) UseMiddleware(mws ...Middleware) {

rou.Use(middlewares...)
}

type staticFileConfig struct {
directoryName string
}

func (rou *Router) AddStaticFiles(endpoint, dirName string) {
cfg := staticFileConfig{directoryName: dirName}

fileServer := http.FileServer(http.Dir(cfg.directoryName))
rou.Router.NewRoute().PathPrefix(endpoint + "/").Handler(http.StripPrefix(endpoint, cfg.staticHandler(fileServer)))
}

func (staticConfig staticFileConfig) staticHandler(fileServer http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
url := r.URL.Path

filePath := strings.Split(url, "/")

fileName := filePath[len(filePath)-1]

const defaultSwaggerFileName = "openapi.json"

if _, err := os.Stat(filepath.Join(staticConfig.directoryName, url)); fileName == defaultSwaggerFileName && err == nil {
w.WriteHeader(http.StatusForbidden)

_, _ = w.Write([]byte("403 forbidden"))

return
}

fileServer.ServeHTTP(w, r)
})
}
Loading

0 comments on commit cd5b92e

Please sign in to comment.