Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Static File Handler #674

Merged
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
807faf9
Added Static File Handler
KedarisettiSreeVamsi Jun 2, 2024
b449cb4
changes to filepath and gofr file
KedarisettiSreeVamsi Jun 2, 2024
7c136e4
Added documentation and removed the example with small edits
KedarisettiSreeVamsi Jun 3, 2024
749f34a
Merge branch 'development' into staticFileHandler
Umang01-hash Jun 4, 2024
25c7cc2
Made changes to Doc file and removed config line
KedarisettiSreeVamsi Jun 4, 2024
fb04cc6
Merge branch 'staticFileHandler' of https://github.com/KedarisettiSre…
KedarisettiSreeVamsi Jun 4, 2024
6e5a7be
typo fix
KedarisettiSreeVamsi Jun 4, 2024
380aada
Merge branch 'development' into staticFileHandler
Umang01-hash Jun 6, 2024
fbbb2c6
Added test for static handling
KedarisettiSreeVamsi Jun 6, 2024
f4ec872
Merge branch 'staticFileHandler' of https://github.com/KedarisettiSre…
KedarisettiSreeVamsi Jun 6, 2024
761e814
minor changes
KedarisettiSreeVamsi Jun 6, 2024
b7605e6
Changes
KedarisettiSreeVamsi Jun 6, 2024
7105889
Merge branch 'development' into staticFileHandler
Umang01-hash Jun 7, 2024
b7f7cbf
made changes
KedarisettiSreeVamsi Jun 7, 2024
04e00b9
Merge branch 'staticFileHandler' of https://github.com/KedarisettiSre…
KedarisettiSreeVamsi Jun 7, 2024
a2b06f9
code quality checks
KedarisettiSreeVamsi Jun 7, 2024
1d34bcc
rework code quality checks
KedarisettiSreeVamsi Jun 7, 2024
0115e03
code quality clearance
KedarisettiSreeVamsi Jun 7, 2024
070f02a
Merge branch 'development' into staticFileHandler
Umang01-hash Jun 10, 2024
aa5288f
resolve review comments
Umang01-hash Jun 10, 2024
fc6a232
Merge branch 'development' of github.com:gofr-dev/gofr into staticFil…
Umang01-hash Jun 10, 2024
7d97572
Merge branch 'development' into staticFileHandler
aryanmehrotra Jun 10, 2024
4c52caf
refactor tests
aryanmehrotra Jun 10, 2024
e75d5b5
fix docs, tests, logs
srijan-27 Jun 11, 2024
9d8ff4b
Merge branch 'development' into staticFileHandler
srijan-27 Jun 11, 2024
cec9b74
Merge branch 'development' into staticFileHandler
aryanmehrotra Jun 12, 2024
ca1122b
add config to static file handling
KedarisettiSreeVamsi Jun 12, 2024
58ccbc0
update of test
KedarisettiSreeVamsi Jun 12, 2024
105543f
fixing for index file handling
KedarisettiSreeVamsi Jun 12, 2024
438bfa3
updates to documentation and code quality fixes
KedarisettiSreeVamsi Jun 12, 2024
0a5fb78
changes and fixes
KedarisettiSreeVamsi Jun 13, 2024
824b1bc
small typo in documentation
KedarisettiSreeVamsi Jun 13, 2024
8d8af7a
checking for public endpoint
KedarisettiSreeVamsi Jun 13, 2024
492dbab
fix linters
aryanmehrotra Jun 17, 2024
d4ee4f0
Merge branch 'development' into staticFileHandler
vipul-rawat Jun 19, 2024
8f0dd2c
changes as requested
KedarisettiSreeVamsi Jun 24, 2024
5aa6105
conflict check
KedarisettiSreeVamsi Jun 24, 2024
cd82021
resolve merge conflicts
aryanmehrotra Jun 25, 2024
e6bbd59
fix reveiw comments
aryanmehrotra Jun 25, 2024
9b9a6ba
fix issues in rendering file
aryanmehrotra Jun 25, 2024
39597ee
remove unwanted exported methods
aryanmehrotra Jun 25, 2024
d92b6f1
refactor implementatins
aryanmehrotra Jun 25, 2024
b25c4b6
unexport static file config function
aryanmehrotra Jun 25, 2024
886e3ef
remove unwanted files
aryanmehrotra Jun 25, 2024
aa6d989
Merge branch 'development' into staticFileHandler
aryanmehrotra Jun 26, 2024
8054eec
add test for invalid file and custom route
aryanmehrotra Jun 26, 2024
f725bc4
update invalid filepath test
aryanmehrotra Jun 26, 2024
d8be25c
add test and remove unwanted code
aryanmehrotra Jun 26, 2024
a45db0f
resolve linters
aryanmehrotra Jun 26, 2024
683952f
fix test
aryanmehrotra Jun 26, 2024
f2d186e
keep inline
aryanmehrotra Jun 26, 2024
bee9a39
Merge branch 'development' into staticFileHandler
aryanmehrotra Jun 26, 2024
fc4ffe3
fix reveiw comments
aryanmehrotra Jun 26, 2024
ec4be3f
Merge branch 'development' into staticFileHandler
aryanmehrotra Jun 26, 2024
c30629e
Merge branch 'development' into staticFileHandler
srijan-27 Jun 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
srijan-27 marked this conversation as resolved.
Show resolved Hide resolved

go app.Run()
srijan-27 marked this conversation as resolved.
Show resolved Hide resolved
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()
aryanmehrotra marked this conversation as resolved.
Show resolved Hide resolved
}
}

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, errFile := os.Create(filepath.Join(directory, indexHTML))

if errFile != nil {
aryanmehrotra marked this conversation as resolved.
Show resolved Hide resolved
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()
aryanmehrotra marked this conversation as resolved.
Show resolved Hide resolved
}
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
Loading