5
5
package public
6
6
7
7
import (
8
+ "encoding/base64"
9
+ "log"
10
+ "net/http"
8
11
"path"
12
+ "path/filepath"
13
+ "strings"
14
+ "time"
9
15
10
16
"code.gitea.io/gitea/modules/setting"
11
17
"gopkg.in/macaron.v1"
@@ -19,15 +25,135 @@ import (
19
25
// Options represents the available options to configure the macaron handler.
20
26
type Options struct {
21
27
Directory string
28
+ IndexFile string
22
29
SkipLogging bool
30
+ // if set to true, will enable caching. Expires header will also be set to
31
+ // expire after the defined time.
32
+ ExpiresAfter time.Duration
33
+ FileSystem http.FileSystem
34
+ Prefix string
23
35
}
24
36
25
37
// Custom implements the macaron static handler for serving custom assets.
26
38
func Custom (opts * Options ) macaron.Handler {
27
- return macaron .Static (
28
- path .Join (setting .CustomPath , "public" ),
29
- macaron.StaticOptions {
30
- SkipLogging : opts .SkipLogging ,
31
- },
32
- )
39
+ return opts .staticHandler (path .Join (setting .CustomPath , "public" ))
40
+ }
41
+
42
+ // staticFileSystem implements http.FileSystem interface.
43
+ type staticFileSystem struct {
44
+ dir * http.Dir
45
+ }
46
+
47
+ func newStaticFileSystem (directory string ) staticFileSystem {
48
+ if ! filepath .IsAbs (directory ) {
49
+ directory = filepath .Join (macaron .Root , directory )
50
+ }
51
+ dir := http .Dir (directory )
52
+ return staticFileSystem {& dir }
53
+ }
54
+
55
+ func (fs staticFileSystem ) Open (name string ) (http.File , error ) {
56
+ return fs .dir .Open (name )
57
+ }
58
+
59
+ // StaticHandler sets up a new middleware for serving static files in the
60
+ func StaticHandler (dir string , opts * Options ) macaron.Handler {
61
+ return opts .staticHandler (dir )
62
+ }
63
+
64
+ func (opts * Options ) staticHandler (dir string ) macaron.Handler {
65
+ // Defaults
66
+ if len (opts .IndexFile ) == 0 {
67
+ opts .IndexFile = "index.html"
68
+ }
69
+ // Normalize the prefix if provided
70
+ if opts .Prefix != "" {
71
+ // Ensure we have a leading '/'
72
+ if opts .Prefix [0 ] != '/' {
73
+ opts .Prefix = "/" + opts .Prefix
74
+ }
75
+ // Remove any trailing '/'
76
+ opts .Prefix = strings .TrimRight (opts .Prefix , "/" )
77
+ }
78
+ if opts .FileSystem == nil {
79
+ opts .FileSystem = newStaticFileSystem (dir )
80
+ }
81
+
82
+ return func (ctx * macaron.Context , log * log.Logger ) {
83
+ opts .handle (ctx , log , opts )
84
+ }
85
+ }
86
+
87
+ func (opts * Options ) handle (ctx * macaron.Context , log * log.Logger , opt * Options ) bool {
88
+ if ctx .Req .Method != "GET" && ctx .Req .Method != "HEAD" {
89
+ return false
90
+ }
91
+
92
+ file := ctx .Req .URL .Path
93
+ // if we have a prefix, filter requests by stripping the prefix
94
+ if opt .Prefix != "" {
95
+ if ! strings .HasPrefix (file , opt .Prefix ) {
96
+ return false
97
+ }
98
+ file = file [len (opt .Prefix ):]
99
+ if file != "" && file [0 ] != '/' {
100
+ return false
101
+ }
102
+ }
103
+
104
+ f , err := opt .FileSystem .Open (file )
105
+ if err != nil {
106
+ return false
107
+ }
108
+ defer f .Close ()
109
+
110
+ fi , err := f .Stat ()
111
+ if err != nil {
112
+ log .Printf ("[Static] %q exists, but fails to open: %v" , file , err )
113
+ return true
114
+ }
115
+
116
+ // Try to serve index file
117
+ if fi .IsDir () {
118
+ // Redirect if missing trailing slash.
119
+ if ! strings .HasSuffix (ctx .Req .URL .Path , "/" ) {
120
+ http .Redirect (ctx .Resp , ctx .Req .Request , ctx .Req .URL .Path + "/" , http .StatusFound )
121
+ return true
122
+ }
123
+
124
+ f , err = opt .FileSystem .Open (file )
125
+ if err != nil {
126
+ return false // Discard error.
127
+ }
128
+ defer f .Close ()
129
+
130
+ fi , err = f .Stat ()
131
+ if err != nil || fi .IsDir () {
132
+ return true
133
+ }
134
+ }
135
+
136
+ if ! opt .SkipLogging {
137
+ log .Println ("[Static] Serving " + file )
138
+ }
139
+
140
+ // Add an Expires header to the static content
141
+ if opt .ExpiresAfter > 0 {
142
+ ctx .Resp .Header ().Set ("Expires" , time .Now ().Add (opt .ExpiresAfter ).UTC ().Format (http .TimeFormat ))
143
+ tag := GenerateETag (string (fi .Size ()), fi .Name (), fi .ModTime ().UTC ().Format (http .TimeFormat ))
144
+ ctx .Resp .Header ().Set ("ETag" , tag )
145
+ if ctx .Req .Header .Get ("If-None-Match" ) == tag {
146
+ ctx .Resp .WriteHeader (304 )
147
+ return false
148
+ }
149
+ }
150
+
151
+ http .ServeContent (ctx .Resp , ctx .Req .Request , file , fi .ModTime (), f )
152
+ return true
153
+ }
154
+
155
+ // GenerateETag generates an ETag based on size, filename and file modification time
156
+ func GenerateETag (fileSize , fileName , modTime string ) string {
157
+ etag := fileSize + fileName + modTime
158
+ return base64 .StdEncoding .EncodeToString ([]byte (etag ))
33
159
}
0 commit comments