Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
caddy
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
caddy
Commits
98d8c0f8
Commit
98d8c0f8
authored
Dec 22, 2015
by
Abiola Ibrahim
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Added new rewrite features.
parent
c748ef94
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
243 additions
and
60 deletions
+243
-60
caddy/setup/rewrite.go
caddy/setup/rewrite.go
+20
-2
caddy/setup/rewrite_test.go
caddy/setup/rewrite_test.go
+10
-10
middleware/rewrite/condition.go
middleware/rewrite/condition.go
+110
-0
middleware/rewrite/rewrite.go
middleware/rewrite/rewrite.go
+33
-45
middleware/rewrite/rewrite_test.go
middleware/rewrite/rewrite_test.go
+2
-3
middleware/rewrite/to.go
middleware/rewrite/to.go
+68
-0
No files found.
caddy/setup/rewrite.go
View file @
98d8c0f8
package
setup
package
setup
import
(
import
(
"net/http"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/rewrite"
"github.com/mholt/caddy/middleware/rewrite"
)
)
...
@@ -13,7 +15,11 @@ func Rewrite(c *Controller) (middleware.Middleware, error) {
...
@@ -13,7 +15,11 @@ func Rewrite(c *Controller) (middleware.Middleware, error) {
}
}
return
func
(
next
middleware
.
Handler
)
middleware
.
Handler
{
return
func
(
next
middleware
.
Handler
)
middleware
.
Handler
{
return
rewrite
.
Rewrite
{
Next
:
next
,
Rules
:
rewrites
}
return
rewrite
.
Rewrite
{
Next
:
next
,
FileSys
:
http
.
Dir
(
c
.
Root
),
Rules
:
rewrites
,
}
},
nil
},
nil
}
}
...
@@ -30,6 +36,8 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
...
@@ -30,6 +36,8 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
args
:=
c
.
RemainingArgs
()
args
:=
c
.
RemainingArgs
()
var
ifs
[]
rewrite
.
If
switch
len
(
args
)
{
switch
len
(
args
)
{
case
2
:
case
2
:
rule
=
rewrite
.
NewSimpleRule
(
args
[
0
],
args
[
1
])
rule
=
rewrite
.
NewSimpleRule
(
args
[
0
],
args
[
1
])
...
@@ -56,6 +64,16 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
...
@@ -56,6 +64,16 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
return
nil
,
c
.
ArgErr
()
return
nil
,
c
.
ArgErr
()
}
}
ext
=
args1
ext
=
args1
case
"if"
:
args1
:=
c
.
RemainingArgs
()
if
len
(
args1
)
!=
3
{
return
nil
,
c
.
ArgErr
()
}
ifCond
,
err
:=
rewrite
.
NewIf
(
args1
[
0
],
args1
[
1
],
args1
[
2
])
if
err
!=
nil
{
return
nil
,
err
}
ifs
=
append
(
ifs
,
ifCond
)
default
:
default
:
return
nil
,
c
.
ArgErr
()
return
nil
,
c
.
ArgErr
()
}
}
...
@@ -64,7 +82,7 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
...
@@ -64,7 +82,7 @@ func rewriteParse(c *Controller) ([]rewrite.Rule, error) {
if
pattern
==
""
||
to
==
""
{
if
pattern
==
""
||
to
==
""
{
return
nil
,
c
.
ArgErr
()
return
nil
,
c
.
ArgErr
()
}
}
if
rule
,
err
=
rewrite
.
New
RegexpRule
(
base
,
pattern
,
to
,
ext
);
err
!=
nil
{
if
rule
,
err
=
rewrite
.
New
ComplexRule
(
base
,
pattern
,
to
,
ext
,
ifs
);
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
regexpRules
=
append
(
regexpRules
,
rule
)
regexpRules
=
append
(
regexpRules
,
rule
)
...
...
caddy/setup/rewrite_test.go
View file @
98d8c0f8
...
@@ -98,14 +98,14 @@ func TestRewriteParse(t *testing.T) {
...
@@ -98,14 +98,14 @@ func TestRewriteParse(t *testing.T) {
r .*
r .*
to /to
to /to
}`
,
false
,
[]
rewrite
.
Rule
{
}`
,
false
,
[]
rewrite
.
Rule
{
&
rewrite
.
Regexp
Rule
{
Base
:
"/"
,
To
:
"/to"
,
Regexp
:
regexp
.
MustCompile
(
".*"
)},
&
rewrite
.
Complex
Rule
{
Base
:
"/"
,
To
:
"/to"
,
Regexp
:
regexp
.
MustCompile
(
".*"
)},
}},
}},
{
`rewrite {
{
`rewrite {
regexp .*
regexp .*
to /to
to /to
ext / html txt
ext / html txt
}`
,
false
,
[]
rewrite
.
Rule
{
}`
,
false
,
[]
rewrite
.
Rule
{
&
rewrite
.
Regexp
Rule
{
Base
:
"/"
,
To
:
"/to"
,
Exts
:
[]
string
{
"/"
,
"html"
,
"txt"
},
Regexp
:
regexp
.
MustCompile
(
".*"
)},
&
rewrite
.
Complex
Rule
{
Base
:
"/"
,
To
:
"/to"
,
Exts
:
[]
string
{
"/"
,
"html"
,
"txt"
},
Regexp
:
regexp
.
MustCompile
(
".*"
)},
}},
}},
{
`rewrite /path {
{
`rewrite /path {
r rr
r rr
...
@@ -116,26 +116,26 @@ func TestRewriteParse(t *testing.T) {
...
@@ -116,26 +116,26 @@ func TestRewriteParse(t *testing.T) {
to /to
to /to
}
}
`
,
false
,
[]
rewrite
.
Rule
{
`
,
false
,
[]
rewrite
.
Rule
{
&
rewrite
.
Regexp
Rule
{
Base
:
"/path"
,
To
:
"/dest"
,
Regexp
:
regexp
.
MustCompile
(
"rr"
)},
&
rewrite
.
Complex
Rule
{
Base
:
"/path"
,
To
:
"/dest"
,
Regexp
:
regexp
.
MustCompile
(
"rr"
)},
&
rewrite
.
Regexp
Rule
{
Base
:
"/"
,
To
:
"/to"
,
Regexp
:
regexp
.
MustCompile
(
"[a-z]+"
)},
&
rewrite
.
Complex
Rule
{
Base
:
"/"
,
To
:
"/to"
,
Regexp
:
regexp
.
MustCompile
(
"[a-z]+"
)},
}},
}},
{
`rewrite {
{
`rewrite {
to /to
to /to
}`
,
true
,
[]
rewrite
.
Rule
{
}`
,
true
,
[]
rewrite
.
Rule
{
&
rewrite
.
Regexp
Rule
{},
&
rewrite
.
Complex
Rule
{},
}},
}},
{
`rewrite {
{
`rewrite {
r .*
r .*
}`
,
true
,
[]
rewrite
.
Rule
{
}`
,
true
,
[]
rewrite
.
Rule
{
&
rewrite
.
Regexp
Rule
{},
&
rewrite
.
Complex
Rule
{},
}},
}},
{
`rewrite {
{
`rewrite {
}`
,
true
,
[]
rewrite
.
Rule
{
}`
,
true
,
[]
rewrite
.
Rule
{
&
rewrite
.
Regexp
Rule
{},
&
rewrite
.
Complex
Rule
{},
}},
}},
{
`rewrite /`
,
true
,
[]
rewrite
.
Rule
{
{
`rewrite /`
,
true
,
[]
rewrite
.
Rule
{
&
rewrite
.
Regexp
Rule
{},
&
rewrite
.
Complex
Rule
{},
}},
}},
}
}
...
@@ -157,8 +157,8 @@ func TestRewriteParse(t *testing.T) {
...
@@ -157,8 +157,8 @@ func TestRewriteParse(t *testing.T) {
}
}
for
j
,
e
:=
range
test
.
expected
{
for
j
,
e
:=
range
test
.
expected
{
actualRule
:=
actual
[
j
]
.
(
*
rewrite
.
Regexp
Rule
)
actualRule
:=
actual
[
j
]
.
(
*
rewrite
.
Complex
Rule
)
expectedRule
:=
e
.
(
*
rewrite
.
Regexp
Rule
)
expectedRule
:=
e
.
(
*
rewrite
.
Complex
Rule
)
if
actualRule
.
Base
!=
expectedRule
.
Base
{
if
actualRule
.
Base
!=
expectedRule
.
Base
{
t
.
Errorf
(
"Test %d, rule %d: Expected Base=%s, got %s"
,
t
.
Errorf
(
"Test %d, rule %d: Expected Base=%s, got %s"
,
...
...
middleware/rewrite/condition.go
0 → 100644
View file @
98d8c0f8
package
rewrite
import
(
"fmt"
"github.com/mholt/caddy/middleware"
"net/http"
"regexp"
"strings"
)
const
(
// Operators
Is
=
"is"
Not
=
"not"
Has
=
"has"
StartsWith
=
"starts_with"
EndsWith
=
"ends_with"
Match
=
"match"
)
func
operatorError
(
operator
string
)
error
{
return
fmt
.
Errorf
(
"Invalid operator"
,
operator
)
}
func
newReplacer
(
r
*
http
.
Request
)
middleware
.
Replacer
{
return
middleware
.
NewReplacer
(
r
,
nil
,
""
)
}
// condition is a rewrite condition.
type
condition
func
(
string
,
string
)
bool
var
conditions
=
map
[
string
]
condition
{
Is
:
isFunc
,
Not
:
notFunc
,
Has
:
hasFunc
,
StartsWith
:
startsWithFunc
,
EndsWith
:
endsWithFunc
,
Match
:
matchFunc
,
}
// isFunc is condition for Is operator.
// It checks for equality.
func
isFunc
(
a
,
b
string
)
bool
{
return
a
==
b
}
// notFunc is condition for Not operator.
// It checks for inequality.
func
notFunc
(
a
,
b
string
)
bool
{
return
a
!=
b
}
// hasFunc is condition for Has operator.
// It checks if b is a substring of a.
func
hasFunc
(
a
,
b
string
)
bool
{
return
strings
.
Contains
(
a
,
b
)
}
// startsWithFunc is condition for StartsWith operator.
// It checks if b is a prefix of a.
func
startsWithFunc
(
a
,
b
string
)
bool
{
return
strings
.
HasPrefix
(
a
,
b
)
}
// endsWithFunc is condition for EndsWith operator.
// It checks if b is a suffix of a.
func
endsWithFunc
(
a
,
b
string
)
bool
{
return
strings
.
HasSuffix
(
a
,
b
)
}
// matchFunc is condition for Match operator.
// It does regexp matching of
func
matchFunc
(
a
,
b
string
)
bool
{
matched
,
_
:=
regexp
.
MatchString
(
b
,
a
)
return
matched
}
// If is statement for a rewrite condition.
type
If
struct
{
A
string
Operator
string
B
string
}
// True returns true if the condition is true and false otherwise.
// If r is not nil, it replaces placeholders before comparison.
func
(
i
If
)
True
(
r
*
http
.
Request
)
bool
{
if
c
,
ok
:=
conditions
[
i
.
Operator
];
ok
{
a
,
b
:=
i
.
A
,
i
.
B
if
r
!=
nil
{
replacer
:=
newReplacer
(
r
)
a
=
replacer
.
Replace
(
i
.
A
)
b
=
replacer
.
Replace
(
i
.
B
)
}
return
c
(
a
,
b
)
}
return
false
}
// NewIf creates a new If condition.
func
NewIf
(
a
,
operator
,
b
string
)
(
If
,
error
)
{
if
_
,
ok
:=
conditions
[
operator
];
!
ok
{
return
If
{},
operatorError
(
operator
)
}
return
If
{
A
:
a
,
Operator
:
operator
,
B
:
b
,
},
nil
}
middleware/rewrite/rewrite.go
View file @
98d8c0f8
...
@@ -5,7 +5,6 @@ package rewrite
...
@@ -5,7 +5,6 @@ package rewrite
import
(
import
(
"fmt"
"fmt"
"net/http"
"net/http"
"net/url"
"path"
"path"
"path/filepath"
"path/filepath"
"regexp"
"regexp"
...
@@ -16,14 +15,15 @@ import (
...
@@ -16,14 +15,15 @@ import (
// Rewrite is middleware to rewrite request locations internally before being handled.
// Rewrite is middleware to rewrite request locations internally before being handled.
type
Rewrite
struct
{
type
Rewrite
struct
{
Next
middleware
.
Handler
Next
middleware
.
Handler
Rules
[]
Rule
FileSys
http
.
FileSystem
Rules
[]
Rule
}
}
// ServeHTTP implements the middleware.Handler interface.
// ServeHTTP implements the middleware.Handler interface.
func
(
rw
Rewrite
)
ServeHTTP
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
(
int
,
error
)
{
func
(
rw
Rewrite
)
ServeHTTP
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
(
int
,
error
)
{
for
_
,
rule
:=
range
rw
.
Rules
{
for
_
,
rule
:=
range
rw
.
Rules
{
if
ok
:=
rule
.
Rewrite
(
r
);
ok
{
if
ok
:=
rule
.
Rewrite
(
r
w
.
FileSys
,
r
);
ok
{
break
break
}
}
}
}
...
@@ -33,7 +33,7 @@ func (rw Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
...
@@ -33,7 +33,7 @@ func (rw Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
// Rule describes an internal location rewrite rule.
// Rule describes an internal location rewrite rule.
type
Rule
interface
{
type
Rule
interface
{
// Rewrite rewrites the internal location of the current request.
// Rewrite rewrites the internal location of the current request.
Rewrite
(
*
http
.
Request
)
bool
Rewrite
(
http
.
FileSystem
,
*
http
.
Request
)
bool
}
}
// SimpleRule is a simple rewrite rule.
// SimpleRule is a simple rewrite rule.
...
@@ -47,23 +47,20 @@ func NewSimpleRule(from, to string) SimpleRule {
...
@@ -47,23 +47,20 @@ func NewSimpleRule(from, to string) SimpleRule {
}
}
// Rewrite rewrites the internal location of the current request.
// Rewrite rewrites the internal location of the current request.
func
(
s
SimpleRule
)
Rewrite
(
r
*
http
.
Request
)
bool
{
func
(
s
SimpleRule
)
Rewrite
(
fs
http
.
FileSystem
,
r
*
http
.
Request
)
bool
{
if
s
.
From
==
r
.
URL
.
Path
{
if
s
.
From
==
r
.
URL
.
Path
{
// take note of this rewrite for internal use by fastcgi
// take note of this rewrite for internal use by fastcgi
// all we need is the URI, not full URL
// all we need is the URI, not full URL
r
.
Header
.
Set
(
headerFieldName
,
r
.
URL
.
RequestURI
())
r
.
Header
.
Set
(
headerFieldName
,
r
.
URL
.
RequestURI
())
// replace variables
// attempt rewrite
to
:=
path
.
Clean
(
middleware
.
NewReplacer
(
r
,
nil
,
""
)
.
Replace
(
s
.
To
))
return
To
(
fs
,
r
,
s
.
To
)
r
.
URL
.
Path
=
to
return
true
}
}
return
false
return
false
}
}
//
Regexp
Rule is a rewrite rule based on a regular expression
//
Complex
Rule is a rewrite rule based on a regular expression
type
Regexp
Rule
struct
{
type
Complex
Rule
struct
{
// Path base. Request to this path and subpaths will be rewritten
// Path base. Request to this path and subpaths will be rewritten
Base
string
Base
string
...
@@ -73,18 +70,26 @@ type RegexpRule struct {
...
@@ -73,18 +70,26 @@ type RegexpRule struct {
// Extensions to filter by
// Extensions to filter by
Exts
[]
string
Exts
[]
string
// Rewrite conditions
Ifs
[]
If
*
regexp
.
Regexp
*
regexp
.
Regexp
}
}
// NewRegexpRule creates a new RegexpRule. It returns an error if regexp
// NewRegexpRule creates a new RegexpRule. It returns an error if regexp
// pattern (pattern) or extensions (ext) are invalid.
// pattern (pattern) or extensions (ext) are invalid.
func
NewRegexpRule
(
base
,
pattern
,
to
string
,
ext
[]
string
)
(
*
RegexpRule
,
error
)
{
func
NewComplexRule
(
base
,
pattern
,
to
string
,
ext
[]
string
,
ifs
[]
If
)
(
*
ComplexRule
,
error
)
{
r
,
err
:=
regexp
.
Compile
(
pattern
)
// validate regexp if present
if
err
!=
nil
{
var
r
*
regexp
.
Regexp
return
nil
,
err
if
pattern
!=
""
{
var
err
error
r
,
err
=
regexp
.
Compile
(
pattern
)
if
err
!=
nil
{
return
nil
,
err
}
}
}
// validate extensions
// validate extensions
if present
for
_
,
v
:=
range
ext
{
for
_
,
v
:=
range
ext
{
if
len
(
v
)
<
2
||
(
len
(
v
)
<
3
&&
v
[
0
]
==
'!'
)
{
if
len
(
v
)
<
2
||
(
len
(
v
)
<
3
&&
v
[
0
]
==
'!'
)
{
// check if no extension is specified
// check if no extension is specified
...
@@ -94,16 +99,17 @@ func NewRegexpRule(base, pattern, to string, ext []string) (*RegexpRule, error)
...
@@ -94,16 +99,17 @@ func NewRegexpRule(base, pattern, to string, ext []string) (*RegexpRule, error)
}
}
}
}
return
&
RegexpRule
{
return
&
ComplexRule
{
base
,
Base
:
base
,
to
,
To
:
to
,
ext
,
Exts
:
ext
,
r
,
Ifs
:
ifs
,
Regexp
:
r
,
},
nil
},
nil
}
}
// Rewrite rewrites the internal location of the current request.
// Rewrite rewrites the internal location of the current request.
func
(
r
*
RegexpRule
)
Rewrite
(
req
*
http
.
Request
)
bool
{
func
(
r
*
ComplexRule
)
Rewrite
(
fs
http
.
FileSystem
,
req
*
http
.
Request
)
bool
{
rPath
:=
req
.
URL
.
Path
rPath
:=
req
.
URL
.
Path
// validate base
// validate base
...
@@ -127,31 +133,13 @@ func (r *RegexpRule) Rewrite(req *http.Request) bool {
...
@@ -127,31 +133,13 @@ func (r *RegexpRule) Rewrite(req *http.Request) bool {
return
false
return
false
}
}
// replace variables
// attempt rewrite
to
:=
path
.
Clean
(
middleware
.
NewReplacer
(
req
,
nil
,
""
)
.
Replace
(
r
.
To
))
return
To
(
fs
,
req
,
r
.
To
)
// validate resulting path
url
,
err
:=
url
.
Parse
(
to
)
if
err
!=
nil
{
return
false
}
// take note of this rewrite for internal use by fastcgi
// all we need is the URI, not full URL
req
.
Header
.
Set
(
headerFieldName
,
req
.
URL
.
RequestURI
())
// perform rewrite
req
.
URL
.
Path
=
url
.
Path
if
url
.
RawQuery
!=
""
{
// overwrite query string if present
req
.
URL
.
RawQuery
=
url
.
RawQuery
}
return
true
}
}
// matchExt matches rPath against registered file extensions.
// matchExt matches rPath against registered file extensions.
// Returns true if a match is found and false otherwise.
// Returns true if a match is found and false otherwise.
func
(
r
*
Regexp
Rule
)
matchExt
(
rPath
string
)
bool
{
func
(
r
*
Complex
Rule
)
matchExt
(
rPath
string
)
bool
{
f
:=
filepath
.
Base
(
rPath
)
f
:=
filepath
.
Base
(
rPath
)
ext
:=
path
.
Ext
(
f
)
ext
:=
path
.
Ext
(
f
)
if
ext
==
""
{
if
ext
==
""
{
...
...
middleware/rewrite/rewrite_test.go
View file @
98d8c0f8
...
@@ -4,9 +4,8 @@ import (
...
@@ -4,9 +4,8 @@ import (
"fmt"
"fmt"
"net/http"
"net/http"
"net/http/httptest"
"net/http/httptest"
"testing"
"strings"
"strings"
"testing"
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware"
)
)
...
@@ -38,7 +37,7 @@ func TestRewrite(t *testing.T) {
...
@@ -38,7 +37,7 @@ func TestRewrite(t *testing.T) {
if
s
:=
strings
.
Split
(
regexpRule
[
3
],
"|"
);
len
(
s
)
>
1
{
if
s
:=
strings
.
Split
(
regexpRule
[
3
],
"|"
);
len
(
s
)
>
1
{
ext
=
s
[
:
len
(
s
)
-
1
]
ext
=
s
[
:
len
(
s
)
-
1
]
}
}
rule
,
err
:=
New
Regexp
Rule
(
regexpRule
[
0
],
regexpRule
[
1
],
regexpRule
[
2
],
ext
)
rule
,
err
:=
New
Complex
Rule
(
regexpRule
[
0
],
regexpRule
[
1
],
regexpRule
[
2
],
ext
)
if
err
!=
nil
{
if
err
!=
nil
{
t
.
Fatal
(
err
)
t
.
Fatal
(
err
)
}
}
...
...
middleware/rewrite/to.go
0 → 100644
View file @
98d8c0f8
package
rewrite
import
(
"log"
"net/http"
"net/url"
"strings"
)
// To attempts rewrite. It attempts to rewrite to first valid path
// or the last path if none of the paths are valid.
// Returns true if rewrite is successful and false otherwise.
func
To
(
fs
http
.
FileSystem
,
r
*
http
.
Request
,
to
string
)
bool
{
tos
:=
strings
.
Fields
(
to
)
replacer
:=
newReplacer
(
r
)
// try each rewrite paths
t
:=
""
for
_
,
v
:=
range
tos
{
t
=
replacer
.
Replace
(
v
)
if
isValidFile
(
fs
,
t
)
{
break
}
}
// validate resulting path
u
,
err
:=
url
.
Parse
(
t
)
if
err
!=
nil
{
// Let the user know we got here. Rewrite is expected but
// the resulting url is invalid.
log
.
Printf
(
"[ERROR] rewrite: resulting path '%v' is invalid. error: %v"
,
t
,
err
)
return
false
}
// take note of this rewrite for internal use by fastcgi
// all we need is the URI, not full URL
r
.
Header
.
Set
(
headerFieldName
,
r
.
URL
.
RequestURI
())
// perform rewrite
r
.
URL
.
Path
=
u
.
Path
if
u
.
RawQuery
!=
""
{
// overwrite query string if present
r
.
URL
.
RawQuery
=
u
.
RawQuery
}
if
u
.
Fragment
!=
""
{
// overwrite fragment if present
r
.
URL
.
Fragment
=
u
.
Fragment
}
return
true
}
// isValidFile checks if file exists on the filesystem.
// if file ends with `/`, it is validated as a directory.
func
isValidFile
(
fs
http
.
FileSystem
,
file
string
)
bool
{
f
,
err
:=
fs
.
Open
(
file
)
if
err
!=
nil
{
return
false
}
defer
f
.
Close
()
stat
,
err
:=
f
.
Stat
()
if
err
!=
nil
{
return
false
}
return
strings
.
HasSuffix
(
file
,
"/"
)
&&
stat
.
IsDir
()
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment