@ -19,6 +19,7 @@ import (
"net/http"
"os"
"path"
"path/filepath"
"strings"
"time"
@ -34,7 +35,7 @@ func init() {
// MatchFile is an HTTP request matcher that can match
// requests based upon file existence.
//
// Upon matching, two new placeholders will be made
// Upon matching, three new placeholders will be made
// available:
//
// - `{http.matchers.file.relative}` The root-relative
@ -42,6 +43,8 @@ func init() {
// requests.
// - `{http.matchers.file.absolute}` The absolute path
// of the matched file.
// - `{http.matchers.file.type}` Set to "directory" if
// the matched file is a directory, "file" otherwise.
type MatchFile struct {
// The root directory, used for creating absolute
// file paths, and required when working with
@ -153,25 +156,18 @@ func (m MatchFile) Validate() error {
}
// Match returns true if r matches m. Returns true
// if a file was matched. If so, two placeholders
// if a file was matched. If so, three placeholders
// will be available:
// - http.matchers.file.relative
// - http.matchers.file.absolute
// - http.matchers.file.type
func ( m MatchFile ) Match ( r * http . Request ) bool {
repl := r . Context ( ) . Value ( caddy . ReplacerCtxKey ) . ( * caddy . Replacer )
rel , abs , matched := m . selectFile ( r )
if matched {
repl . Set ( "http.matchers.file.relative" , rel )
repl . Set ( "http.matchers.file.absolute" , abs )
}
return matched
return m . selectFile ( r )
}
// selectFile chooses a file according to m.TryPolicy by appending
// the paths in m.TryFiles to m.Root, with placeholder replacements.
// It returns the root-relative path to the matched file, the full
// or absolute path, and whether a match was made.
func ( m MatchFile ) selectFile ( r * http . Request ) ( rel , abs string , matched bool ) {
func ( m MatchFile ) selectFile ( r * http . Request ) ( matched bool ) {
repl := r . Context ( ) . Value ( caddy . ReplacerCtxKey ) . ( * caddy . Replacer )
root := repl . ReplaceAll ( m . Root , "." )
@ -183,13 +179,35 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) {
m . TryFiles = [ ] string { r . URL . Path }
}
// common preparation of the file into parts
prepareFilePath := func ( file string ) ( string , string ) {
suffix := m . firstSplit ( path . Clean ( repl . ReplaceAll ( file , "" ) ) )
if strings . HasSuffix ( file , "/" ) {
suffix += "/"
}
fullpath := sanitizedPathJoin ( root , suffix )
return suffix , fullpath
}
// sets up the placeholders for the matched file
setPlaceholders := func ( info os . FileInfo , rel string , abs string ) {
repl . Set ( "http.matchers.file.relative" , rel )
repl . Set ( "http.matchers.file.absolute" , abs )
fileType := "file"
if info . IsDir ( ) {
fileType = "directory"
}
repl . Set ( "http.matchers.file.type" , fileType )
}
switch m . TryPolicy {
case "" , tryPolicyFirstExist :
for _ , f := range m . TryFiles {
suffix := m . firstSplit ( path . Clean ( repl . ReplaceAll ( f , "" ) ) )
fullpath := sanitizedPathJoin ( root , suffix )
if strictFileExists ( fullpath ) {
return suffix , fullpath , true
suffix , fullpath := prepareFilePath ( f )
if info , exists := strictFileExists ( fullpath ) ; exists {
setPlaceholders ( info , suffix , fullpath )
return true
}
}
@ -197,9 +215,9 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) {
var largestSize int64
var largestFilename string
var largestSuffix string
var info os . FileInfo
for _ , f := range m . TryFiles {
suffix := m . firstSplit ( path . Clean ( repl . ReplaceAll ( f , "" ) ) )
fullpath := sanitizedPathJoin ( root , suffix )
suffix , fullpath := prepareFilePath ( f )
info , err := os . Stat ( fullpath )
if err == nil && info . Size ( ) > largestSize {
largestSize = info . Size ( )
@ -207,15 +225,16 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) {
largestSuffix = suffix
}
}
return largestSuffix , largestFilename , true
setPlaceholders ( info , largestSuffix , largestFilename )
return true
case tryPolicySmallestSize :
var smallestSize int64
var smallestFilename string
var smallestSuffix string
var info os . FileInfo
for _ , f := range m . TryFiles {
suffix := m . firstSplit ( path . Clean ( repl . ReplaceAll ( f , "" ) ) )
fullpath := sanitizedPathJoin ( root , suffix )
suffix , fullpath := prepareFilePath ( f )
info , err := os . Stat ( fullpath )
if err == nil && ( smallestSize == 0 || info . Size ( ) < smallestSize ) {
smallestSize = info . Size ( )
@ -223,15 +242,16 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) {
smallestSuffix = suffix
}
}
return smallestSuffix , smallestFilename , true
setPlaceholders ( info , smallestSuffix , smallestFilename )
return true
case tryPolicyMostRecentlyMod :
var recentDate time . Time
var recentFilename string
var recentSuffix string
var info os . FileInfo
for _ , f := range m . TryFiles {
suffix := m . firstSplit ( path . Clean ( repl . ReplaceAll ( f , "" ) ) )
fullpath := sanitizedPathJoin ( root , suffix )
suffix , fullpath := prepareFilePath ( f )
info , err := os . Stat ( fullpath )
if err == nil &&
( recentDate . IsZero ( ) || info . ModTime ( ) . After ( recentDate ) ) {
@ -240,7 +260,8 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) {
recentSuffix = suffix
}
}
return recentSuffix , recentFilename , true
setPlaceholders ( info , recentSuffix , recentFilename )
return true
}
return
@ -252,7 +273,7 @@ func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) {
// the file must also be a directory; if it does
// NOT end in a forward slash, the file must NOT
// be a directory.
func strictFileExists ( file string ) bool {
func strictFileExists ( file string ) ( os . FileInfo , bool ) {
stat , err := os . Stat ( file )
if err != nil {
// in reality, this can be any error
@ -263,16 +284,16 @@ func strictFileExists(file string) bool {
// the file exists, so we just treat any
// error as if it does not exist; see
// https://stackoverflow.com/a/12518877/1048862
return false
return nil , false
}
if strings . HasSuffix ( file , "/" ) {
if strings . HasSuffix ( file , string ( filepath . Separator ) ) {
// by convention, file paths ending
// in a slash must be a directory
return stat . IsDir ( )
// in a path separator must be a directory
return stat , stat . IsDir ( )
}
// by convention, file paths NOT ending
// in a slash must NOT be a directory
return ! stat . IsDir ( )
// in a path separator must NOT be a directory
return stat , ! stat . IsDir ( )
}
// firstSplit returns the first result where the path