Skip to content

Commit

Permalink
Merge branch 'sb12/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
zekroTJA committed Oct 26, 2023
2 parents 3830882 + 3a32109 commit 051a0f7
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 70 deletions.
126 changes: 126 additions & 0 deletions caching/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package caching

import (
"fmt"
routing "github.com/go-ozzo/ozzo-routing/v2"
"strings"
"time"
)

type AccessType string

const (
AccessPublic = AccessType("public")
AccessPrivate = AccessType("private")
)

// Options defines Cache Control directive values as defined in
// RFC 7234 Section 5.2.1.
//
// https://www.rfc-editor.org/rfc/rfc7234#section-5.2.1
//
// Additional Reference:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#directives
type Options struct {
Access AccessType
MaxAge time.Duration
SMaxAge time.Duration
NoCache bool
NoStore bool
MustRevalidate bool
ProxyRevalidate bool
MustUnderstand bool
NoTransform bool
Immutable bool
}

func (t Options) BuildHeaderValues() string {
var sb strings.Builder

if t.Access != "" {
fmt.Fprintf(&sb, "%s,", t.Access)
}

if t.MaxAge != 0 {
fmt.Fprintf(&sb, "max-age=%d,", int64(t.MaxAge.Round(time.Second).Seconds()))
}

if t.SMaxAge != 0 {
fmt.Fprintf(&sb, "s-max-age=%d,", int64(t.SMaxAge.Round(time.Second).Seconds()))
}

if t.NoCache {
fmt.Fprint(&sb, "no-cache,")
}

if t.NoStore {
fmt.Fprint(&sb, "no-store,")
}

if t.MustRevalidate {
fmt.Fprint(&sb, "must-revalidate,")
}

if t.ProxyRevalidate {
fmt.Fprint(&sb, "proxy-revalidate,")
}

if t.MustUnderstand {
fmt.Fprint(&sb, "must-understand,")
}

if t.NoTransform {
fmt.Fprint(&sb, "no-transform,")
}

if t.Immutable {
fmt.Fprint(&sb, "immutable,")
}

v := sb.String()
if len(v) == 0 {
return ""
}

return v[:len(v)-1]
}

// Handler returns a routing.Handler which sets the "Access-Control" header
// value as defined in the given options to the response.
func Handler(options Options) routing.Handler {
headerValue := options.BuildHeaderValues()
return func(c *routing.Context) error {
c.Response.Header().Set("Access-Control", headerValue)
return nil
}
}

// Public returns a routing.Handler which sets the "Access-Control" header
// value to "public,max-age=<maxAge>".
func Public(maxAge time.Duration) routing.Handler {
return Handler(Options{
Access: AccessPublic,
MaxAge: maxAge,
})
}

// Private returns a routing.Handler which sets the "Access-Control" header
// value to "private,max-age=<maxAge>".
func Private(maxAge time.Duration) routing.Handler {
return Handler(Options{
Access: AccessPrivate,
MaxAge: maxAge,
})
}

// NoCache returns a routing.Handler which sets the "Access-Control" header
// value to "no-cache".
func NoCache() routing.Handler {
return Handler(Options{NoCache: true})
}

// NoStore returns a routing.Handler which sets the "Access-Control" header
// value to "no-store".
func NoStore() routing.Handler {
return Handler(Options{NoStore: true})
}
67 changes: 67 additions & 0 deletions caching/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package caching

import (
"testing"
"time"
)

func TestOptions_BuildHeaderValues(t1 *testing.T) {
tests := []struct {
name string
fields Options
want string
}{
{
name: "all",
fields: Options{
Access: AccessPublic,
MaxAge: 5 * time.Minute,
SMaxAge: 10 * time.Minute,
NoCache: true,
NoStore: true,
MustRevalidate: true,
ProxyRevalidate: true,
MustUnderstand: true,
NoTransform: true,
Immutable: true,
},
want: "public,max-age=300,s-max-age=600,no-cache,no-store,must-revalidate,proxy-revalidate,must-understand,no-transform,immutable",
},
{
name: "partial",
fields: Options{
Access: AccessPublic,
MaxAge: 30 * time.Second,
MustRevalidate: true,
},
want: "public,max-age=30,must-revalidate",
},
{
name: "empty",
fields: Options{},
want: "",
},
}

for _, tt := range tests {
t1.Run(tt.name, func(t1 *testing.T) {
t := Options{
Access: tt.fields.Access,
MaxAge: tt.fields.MaxAge,
SMaxAge: tt.fields.SMaxAge,
NoCache: tt.fields.NoCache,
NoStore: tt.fields.NoStore,
MustRevalidate: tt.fields.MustRevalidate,
ProxyRevalidate: tt.fields.ProxyRevalidate,
MustUnderstand: tt.fields.MustUnderstand,
NoTransform: tt.fields.NoTransform,
Immutable: tt.fields.Immutable,
}
if got := t.BuildHeaderValues(); got != tt.want {
t1.Errorf("BuildHeaderValues() failed\n"+
"\twas: %v\n"+
"\twant: %v", got, tt.want)
}
})
}
}
71 changes: 71 additions & 0 deletions file/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package file

import (
"path/filepath"

routing "github.com/go-ozzo/ozzo-routing/v2"
)

// ServerOptions defines the possible options for the Server handler.
type ServerOptions struct {
// The path that all files to be served should be located within. The path map passed to the Server method
// are all relative to this path. This property can be specified as an absolute file path or a path relative
// to the current working path. If not set, this property defaults to the current working path.
RootPath string
// The file (e.g. index.html) to be served when the current request corresponds to a directory.
// If not set, the handler will return a 404 HTTP error when the request corresponds to a directory.
// This should only be a file name without the directory part.
IndexFile string
// The file to be served when no file or directory matches the current request.
// If not set, the handler will return a 404 HTTP error when no file/directory matches the request.
// The path of this file is relative to RootPath
CatchAllFile string
// A function that checks if the requested file path is allowed. If allowed, the function
// may do additional work such as setting Expires HTTP header.
// The function should return a boolean indicating whether the file should be served or not.
// If false, a 404 HTTP error will be returned by the handler.
Allow func(*routing.Context, string) bool
// Define available compression encodings for serving files. Encodings are negotiated against the
// unser agent. The first encoding which matches the accepted encodings from the user agent as well
// as is available as file is served to the client.
Compression []Encoding
}

// Merge takes another instance of ServerOptions and merges it with the current instance.
// Thereby other overwrites values in t if existent. The merged instance is returned as new
// ServerOptions instance.
func (t ServerOptions) Merge(other ServerOptions) (new ServerOptions) {
new = t

if other.Allow != nil {
new.Allow = other.Allow
}
if other.CatchAllFile != "" {
new.CatchAllFile = other.CatchAllFile
}
if other.Compression != nil {
new.Compression = other.Compression
}
if other.IndexFile != "" {
new.IndexFile = other.IndexFile
}
if other.RootPath != "" {
new.RootPath = other.RootPath
}

return new
}

func getServerOptions(opts []ServerOptions) ServerOptions {
var options ServerOptions

for _, opt := range opts {
options = options.Merge(opt)
}

if !filepath.IsAbs(options.RootPath) {
options.RootPath = filepath.Join(RootPath, options.RootPath)
}

return options
}
Loading

0 comments on commit 051a0f7

Please sign in to comment.