55package  public
66
77import  (
8+ 	"encoding/base64" 
9+ 	"log" 
10+ 	"net/http" 
811	"path" 
12+ 	"path/filepath" 
13+ 	"strings" 
14+ 	"time" 
915
1016	"code.gitea.io/gitea/modules/setting" 
1117	"gopkg.in/macaron.v1" 
@@ -19,15 +25,135 @@ import (
1925// Options represents the available options to configure the macaron handler. 
2026type  Options  struct  {
2127	Directory    string 
28+ 	IndexFile    string 
2229	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 
2335}
2436
2537// Custom implements the macaron static handler for serving custom assets. 
2638func  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 ))
33159}
0 commit comments