mirror of
https://github.com/kataras/iris.git
synced 2025-12-18 02:17:05 +00:00
Former-commit-id: e760aa67d8750bfdd15fc84e2dedeb591c204ba9
This commit is contained in:
@@ -1,8 +1,11 @@
|
|||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1>Hi {{.Name}}
|
<h1>Hi {{.Name}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
@@ -71,15 +71,15 @@ func (u UnmarshalerFunc) Unmarshal(data []byte, v interface{}) error {
|
|||||||
return u(data, v)
|
return u(data, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestParams is a key string - value string storage which context's request params should implement.
|
// RequestParams is a key string - value string storage which
|
||||||
// RequestValues is for communication between middleware, RequestParams cannot be changed, are setted at the routing
|
// context's request dynamic path params are being kept.
|
||||||
// time, stores the dynamic named parameters, can be empty if the route is static.
|
// Empty if the route is static.
|
||||||
type RequestParams struct {
|
type RequestParams struct {
|
||||||
store memstore.Store
|
store memstore.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set shouldn't be used as a local storage, context's values store
|
// Set adds a key-value pair to the path parameters values
|
||||||
// is the local storage, not params.
|
// it's being called internally so it shouldn't be used as a local storage by the user, use `ctx.Values()` instead.
|
||||||
func (r *RequestParams) Set(key, value string) {
|
func (r *RequestParams) Set(key, value string) {
|
||||||
r.store.Set(key, value)
|
r.store.Set(key, value)
|
||||||
}
|
}
|
||||||
@@ -97,17 +97,38 @@ func (r RequestParams) Get(key string) string {
|
|||||||
return r.store.GetString(key)
|
return r.store.GetString(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt returns the param's value as int, based on its key.
|
// GetTrim returns a path parameter's value without trailing spaces based on its route's dynamic path key.
|
||||||
|
func (r RequestParams) GetTrim(key string) string {
|
||||||
|
return strings.TrimSpace(r.Get(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEscape returns a path parameter's double-url-query-escaped value based on its route's dynamic path key.
|
||||||
|
func (r RequestParams) GetEscape(key string) string {
|
||||||
|
return DecodeQuery(DecodeQuery(r.Get(key)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDecoded returns a path parameter's double-url-query-escaped value based on its route's dynamic path key.
|
||||||
|
// same as `GetEscape`.
|
||||||
|
func (r RequestParams) GetDecoded(key string) string {
|
||||||
|
return r.GetEscape(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInt returns the path parameter's value as int, based on its key.
|
||||||
func (r RequestParams) GetInt(key string) (int, error) {
|
func (r RequestParams) GetInt(key string) (int, error) {
|
||||||
return r.store.GetInt(key)
|
return r.store.GetInt(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInt64 returns the user's value as int64, based on its key.
|
// GetInt64 returns the path paramete's value as int64, based on its key.
|
||||||
func (r RequestParams) GetInt64(key string) (int64, error) {
|
func (r RequestParams) GetInt64(key string) (int64, error) {
|
||||||
return r.store.GetInt64(key)
|
return r.store.GetInt64(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBool returns the user's value as bool, based on its key.
|
// GetFloat64 returns a path parameter's value based as float64 on its route's dynamic path key.
|
||||||
|
func (r RequestParams) GetFloat64(key string) (float64, error) {
|
||||||
|
return strconv.ParseFloat(r.Get(key), 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBool returns the path parameter's value as bool, based on its key.
|
||||||
// a string which is "1" or "t" or "T" or "TRUE" or "true" or "True"
|
// a string which is "1" or "t" or "T" or "TRUE" or "true" or "True"
|
||||||
// or "0" or "f" or "F" or "FALSE" or "false" or "False".
|
// or "0" or "f" or "F" or "FALSE" or "false" or "False".
|
||||||
// Any other value returns an error.
|
// Any other value returns an error.
|
||||||
@@ -115,11 +136,6 @@ func (r RequestParams) GetBool(key string) (bool, error) {
|
|||||||
return r.store.GetBool(key)
|
return r.store.GetBool(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDecoded returns the url-query-decoded user's value based on its key.
|
|
||||||
func (r RequestParams) GetDecoded(key string) string {
|
|
||||||
return DecodeQuery(DecodeQuery(r.Get(key)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIntUnslashed same as Get but it removes the first slash if found.
|
// GetIntUnslashed same as Get but it removes the first slash if found.
|
||||||
// Usage: Get an id from a wildcard path.
|
// Usage: Get an id from a wildcard path.
|
||||||
//
|
//
|
||||||
@@ -350,12 +366,24 @@ type Context interface {
|
|||||||
|
|
||||||
// URLParam returns the get parameter from a request , if any.
|
// URLParam returns the get parameter from a request , if any.
|
||||||
URLParam(name string) string
|
URLParam(name string) string
|
||||||
|
// URLParamTrim returns the url query parameter with trailing white spaces removed from a request,
|
||||||
|
// returns an error if parse failed.
|
||||||
|
URLParamTrim(name string) string
|
||||||
|
// URLParamTrim returns the escaped url query parameter from a request,
|
||||||
|
// returns an error if parse failed.
|
||||||
|
URLParamEscape(name string) string
|
||||||
// URLParamInt returns the url query parameter as int value from a request,
|
// URLParamInt returns the url query parameter as int value from a request,
|
||||||
// returns an error if parse failed.
|
// returns an error if parse failed.
|
||||||
URLParamInt(name string) (int, error)
|
URLParamInt(name string) (int, error)
|
||||||
// URLParamInt64 returns the url query parameter as int64 value from a request,
|
// URLParamInt64 returns the url query parameter as int64 value from a request,
|
||||||
// returns an error if parse failed.
|
// returns an error if parse failed.
|
||||||
URLParamInt64(name string) (int64, error)
|
URLParamInt64(name string) (int64, error)
|
||||||
|
// URLParamInt64 returns the url query parameter as float64 value from a request,
|
||||||
|
// returns an error if parse failed.
|
||||||
|
URLParamFloat64(name string) (float64, error)
|
||||||
|
// URLParamBool returns the url query parameter as boolean value from a request,
|
||||||
|
// returns an error if parse failed.
|
||||||
|
URLParamBool(name string) (bool, error)
|
||||||
// URLParams returns a map of GET query parameters separated by comma if more than one
|
// URLParams returns a map of GET query parameters separated by comma if more than one
|
||||||
// it returns an empty map if nothing found.
|
// it returns an empty map if nothing found.
|
||||||
URLParams() map[string]string
|
URLParams() map[string]string
|
||||||
@@ -367,9 +395,27 @@ type Context interface {
|
|||||||
//
|
//
|
||||||
// NOTE: A check for nil is necessary.
|
// NOTE: A check for nil is necessary.
|
||||||
FormValues() map[string][]string
|
FormValues() map[string][]string
|
||||||
|
|
||||||
// PostValue returns a form's only-post value by its name,
|
// PostValue returns a form's only-post value by its name,
|
||||||
// same as Request.PostFormValue.
|
// same as Request.PostFormValue.
|
||||||
PostValue(name string) string
|
PostValue(name string) string
|
||||||
|
// PostValueTrim returns a form's only-post value without trailing spaces by its name.
|
||||||
|
PostValueTrim(name string) string
|
||||||
|
// PostValueEscape returns a form's only-post escaped value by its name.
|
||||||
|
PostValueEscape(name string) string
|
||||||
|
// PostValueInt returns a form's only-post value as int by its name.
|
||||||
|
PostValueInt(name string) (int, error)
|
||||||
|
// PostValueInt64 returns a form's only-post value as int64 by its name.
|
||||||
|
PostValueInt64(name string) (int64, error)
|
||||||
|
// PostValueFloat64 returns a form's only-post value as float64 by its name.
|
||||||
|
PostValueFloat64(name string) (float64, error)
|
||||||
|
// PostValue returns a form's only-post value as boolean by its name.
|
||||||
|
PostValueBool(name string) (bool, error)
|
||||||
|
// PostValues returns a form's only-post values.
|
||||||
|
// PostValues calls ParseMultipartForm and ParseForm if necessary and ignores
|
||||||
|
// any errors returned by these functions.
|
||||||
|
PostValues(name string) []string
|
||||||
|
|
||||||
// FormFile returns the first file for the provided form key.
|
// FormFile returns the first file for the provided form key.
|
||||||
// FormFile calls ctx.Request.ParseMultipartForm and ParseForm if necessary.
|
// FormFile calls ctx.Request.ParseMultipartForm and ParseForm if necessary.
|
||||||
//
|
//
|
||||||
@@ -1298,6 +1344,18 @@ func (ctx *context) URLParam(name string) string {
|
|||||||
return ctx.request.URL.Query().Get(name)
|
return ctx.request.URL.Query().Get(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// URLParamTrim returns the url query parameter with trailing white spaces removed from a request,
|
||||||
|
// returns an error if parse failed.
|
||||||
|
func (ctx *context) URLParamTrim(name string) string {
|
||||||
|
return strings.TrimSpace(ctx.URLParam(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// URLParamTrim returns the escaped url query parameter from a request,
|
||||||
|
// returns an error if parse failed.
|
||||||
|
func (ctx *context) URLParamEscape(name string) string {
|
||||||
|
return DecodeQuery(ctx.URLParam(name))
|
||||||
|
}
|
||||||
|
|
||||||
// URLParamInt returns the url query parameter as int value from a request,
|
// URLParamInt returns the url query parameter as int value from a request,
|
||||||
// returns an error if parse failed.
|
// returns an error if parse failed.
|
||||||
func (ctx *context) URLParamInt(name string) (int, error) {
|
func (ctx *context) URLParamInt(name string) (int, error) {
|
||||||
@@ -1310,6 +1368,18 @@ func (ctx *context) URLParamInt64(name string) (int64, error) {
|
|||||||
return strconv.ParseInt(ctx.URLParam(name), 10, 64)
|
return strconv.ParseInt(ctx.URLParam(name), 10, 64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// URLParamInt64 returns the url query parameter as float64 value from a request,
|
||||||
|
// returns an error if parse failed.
|
||||||
|
func (ctx *context) URLParamFloat64(name string) (float64, error) {
|
||||||
|
return strconv.ParseFloat(ctx.URLParam(name), 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// URLParamBool returns the url query parameter as boolean value from a request,
|
||||||
|
// returns an error if parse failed.
|
||||||
|
func (ctx *context) URLParamBool(name string) (bool, error) {
|
||||||
|
return strconv.ParseBool(ctx.URLParam(name))
|
||||||
|
}
|
||||||
|
|
||||||
// URLParams returns a map of GET query parameters separated by comma if more than one
|
// URLParams returns a map of GET query parameters separated by comma if more than one
|
||||||
// it returns an empty map if nothing found.
|
// it returns an empty map if nothing found.
|
||||||
func (ctx *context) URLParams() map[string]string {
|
func (ctx *context) URLParams() map[string]string {
|
||||||
@@ -1358,6 +1428,57 @@ func (ctx *context) PostValue(name string) string {
|
|||||||
return ctx.request.PostFormValue(name)
|
return ctx.request.PostFormValue(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PostValueTrim returns a form's only-post value without trailing spaces by its name.
|
||||||
|
func (ctx *context) PostValueTrim(name string) string {
|
||||||
|
return strings.TrimSpace(ctx.PostValue(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostValueEscape returns a form's only-post escaped value by its name.
|
||||||
|
func (ctx *context) PostValueEscape(name string) string {
|
||||||
|
return DecodeQuery(ctx.PostValue(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostValueInt returns a form's only-post value as int by its name.
|
||||||
|
func (ctx *context) PostValueInt(name string) (int, error) {
|
||||||
|
return strconv.Atoi(ctx.PostValue(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostValueInt64 returns a form's only-post value as int64 by its name.
|
||||||
|
func (ctx *context) PostValueInt64(name string) (int64, error) {
|
||||||
|
return strconv.ParseInt(ctx.PostValue(name), 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostValueFloat64 returns a form's only-post value as float64 by its name.
|
||||||
|
func (ctx *context) PostValueFloat64(name string) (float64, error) {
|
||||||
|
return strconv.ParseFloat(ctx.PostValue(name), 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostValue returns a form's only-post value as boolean by its name.
|
||||||
|
func (ctx *context) PostValueBool(name string) (bool, error) {
|
||||||
|
return strconv.ParseBool(ctx.PostValue(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultMaxMemory is the default value
|
||||||
|
// for post values' max memory, defaults to
|
||||||
|
// 32MB.
|
||||||
|
// Can be also changed by the middleware `LimitRequestBodySize`
|
||||||
|
// or `context#SetMaxRequestBodySize`.
|
||||||
|
DefaultMaxMemory = 32 << 20 // 32 MB
|
||||||
|
)
|
||||||
|
|
||||||
|
// PostValues returns a form's only-post values.
|
||||||
|
// PostValues calls ParseMultipartForm and ParseForm if necessary and ignores
|
||||||
|
// any errors returned by these functions.
|
||||||
|
func (ctx *context) PostValues(name string) []string {
|
||||||
|
r := ctx.request
|
||||||
|
if r.PostForm == nil {
|
||||||
|
r.ParseMultipartForm(DefaultMaxMemory)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.PostForm[name]
|
||||||
|
}
|
||||||
|
|
||||||
// FormFile returns the first file for the provided form key.
|
// FormFile returns the first file for the provided form key.
|
||||||
// FormFile calls ctx.request.ParseMultipartForm and ParseForm if necessary.
|
// FormFile calls ctx.request.ParseMultipartForm and ParseForm if necessary.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -22,6 +22,26 @@ type (
|
|||||||
Error pongo2.Error
|
Error pongo2.Error
|
||||||
// FilterFunction conversion for pongo2.FilterFunction
|
// FilterFunction conversion for pongo2.FilterFunction
|
||||||
FilterFunction func(in *Value, param *Value) (out *Value, err *Error)
|
FilterFunction func(in *Value, param *Value) (out *Value, err *Error)
|
||||||
|
|
||||||
|
// Parser conversion for pongo2.Parser
|
||||||
|
Parser pongo2.Parser
|
||||||
|
// Token conversion for pongo2.Token
|
||||||
|
Token pongo2.Token
|
||||||
|
// INodeTag conversion for pongo2.InodeTag
|
||||||
|
INodeTag pongo2.INodeTag
|
||||||
|
// TagParser the function signature of the tag's parser you will have
|
||||||
|
// to implement in order to create a new tag.
|
||||||
|
//
|
||||||
|
// 'doc' is providing access to the whole document while 'arguments'
|
||||||
|
// is providing access to the user's arguments to the tag:
|
||||||
|
//
|
||||||
|
// {% your_tag_name some "arguments" 123 %}
|
||||||
|
//
|
||||||
|
// start_token will be the *Token with the tag's name in it (here: your_tag_name).
|
||||||
|
//
|
||||||
|
// Please see the Parser documentation on how to use the parser.
|
||||||
|
// See `RegisterTag` for more information about writing a tag as well.
|
||||||
|
TagParser func(doc *Parser, start *Token, arguments *Parser) (INodeTag, *Error)
|
||||||
)
|
)
|
||||||
|
|
||||||
type tDjangoAssetLoader struct {
|
type tDjangoAssetLoader struct {
|
||||||
@@ -119,19 +139,58 @@ func (s *DjangoEngine) AddFunc(funcName string, funcBody interface{}) {
|
|||||||
s.rmu.Unlock()
|
s.rmu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddFilter adds a filter to the template.
|
// AddFilter registers a new filter. If there's already a filter with the same
|
||||||
|
// name, RegisterFilter will panic. You usually want to call this
|
||||||
|
// function in the filter's init() function:
|
||||||
|
// http://golang.org/doc/effective_go.html#init
|
||||||
|
//
|
||||||
|
// Same as `RegisterFilter`.
|
||||||
func (s *DjangoEngine) AddFilter(filterName string, filterBody FilterFunction) *DjangoEngine {
|
func (s *DjangoEngine) AddFilter(filterName string, filterBody FilterFunction) *DjangoEngine {
|
||||||
s.rmu.Lock()
|
return s.registerFilter(filterName, filterBody)
|
||||||
s.filters[filterName] = filterBody
|
}
|
||||||
s.rmu.Unlock()
|
|
||||||
|
// RegisterFilter registers a new filter. If there's already a filter with the same
|
||||||
|
// name, RegisterFilter will panic. You usually want to call this
|
||||||
|
// function in the filter's init() function:
|
||||||
|
// http://golang.org/doc/effective_go.html#init
|
||||||
|
//
|
||||||
|
// See http://www.florian-schlachter.de/post/pongo2/ for more about
|
||||||
|
// writing filters and tags.
|
||||||
|
func (s *DjangoEngine) RegisterFilter(filterName string, filterBody FilterFunction) *DjangoEngine {
|
||||||
|
return s.registerFilter(filterName, filterBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DjangoEngine) registerFilter(filterName string, filterBody FilterFunction) *DjangoEngine {
|
||||||
|
fn := pongo2.FilterFunction(func(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
|
||||||
|
theOut, theErr := filterBody((*Value)(in), (*Value)(param))
|
||||||
|
return (*pongo2.Value)(theOut), (*pongo2.Error)(theErr)
|
||||||
|
})
|
||||||
|
pongo2.RegisterFilter(filterName, fn)
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterTag registers a new tag. You usually want to call this
|
||||||
|
// function in the tag's init() function:
|
||||||
|
// http://golang.org/doc/effective_go.html#init
|
||||||
|
//
|
||||||
|
// See http://www.florian-schlachter.de/post/pongo2/ for more about
|
||||||
|
// writing filters and tags.
|
||||||
|
func (s *DjangoEngine) RegisterTag(tagName string, parserFn TagParser) error {
|
||||||
|
fn := func(doc *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser) (pongo2.INodeTag, *pongo2.Error) {
|
||||||
|
t, err := parserFn((*Parser)(doc), (*Token)(start), (*Parser)(arguments))
|
||||||
|
return t, (*pongo2.Error)(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pongo2.RegisterTag(tagName, fn)
|
||||||
|
}
|
||||||
|
|
||||||
// Load parses the templates to the engine.
|
// Load parses the templates to the engine.
|
||||||
// It's alos responsible to add the necessary global functions.
|
// It's alos responsible to add the necessary global functions.
|
||||||
//
|
//
|
||||||
// Returns an error if something bad happens, user is responsible to catch it.
|
// Returns an error if something bad happens, user is responsible to catch it.
|
||||||
func (s *DjangoEngine) Load() error {
|
func (s *DjangoEngine) Load() error {
|
||||||
|
|
||||||
if s.assetFn != nil && s.namesFn != nil {
|
if s.assetFn != nil && s.namesFn != nil {
|
||||||
// embedded
|
// embedded
|
||||||
return s.loadAssets()
|
return s.loadAssets()
|
||||||
@@ -147,22 +206,6 @@ func (s *DjangoEngine) Load() error {
|
|||||||
return s.loadDirectory()
|
return s.loadDirectory()
|
||||||
}
|
}
|
||||||
|
|
||||||
// this exists because of moving the pongo2 to the vendors without conflictitions if users
|
|
||||||
// wants to register pongo2 filters they can use this django.FilterFunc to do so.
|
|
||||||
func (s *DjangoEngine) convertFilters() map[string]pongo2.FilterFunction {
|
|
||||||
filters := make(map[string]pongo2.FilterFunction, len(s.filters))
|
|
||||||
for k, v := range s.filters {
|
|
||||||
func(filterName string, filterFunc FilterFunction) {
|
|
||||||
fn := pongo2.FilterFunction(func(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
|
|
||||||
theOut, theErr := filterFunc((*Value)(in), (*Value)(param))
|
|
||||||
return (*pongo2.Value)(theOut), (*pongo2.Error)(theErr)
|
|
||||||
})
|
|
||||||
filters[filterName] = fn
|
|
||||||
}(k, v)
|
|
||||||
}
|
|
||||||
return filters
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadDirectory loads the templates from directory.
|
// LoadDirectory loads the templates from directory.
|
||||||
func (s *DjangoEngine) loadDirectory() (templateErr error) {
|
func (s *DjangoEngine) loadDirectory() (templateErr error) {
|
||||||
dir, extension := s.directory, s.extension
|
dir, extension := s.directory, s.extension
|
||||||
@@ -175,12 +218,6 @@ func (s *DjangoEngine) loadDirectory() (templateErr error) {
|
|||||||
set := pongo2.NewSet("", fsLoader)
|
set := pongo2.NewSet("", fsLoader)
|
||||||
set.Globals = getPongoContext(s.globals)
|
set.Globals = getPongoContext(s.globals)
|
||||||
|
|
||||||
// set the filters
|
|
||||||
filters := s.convertFilters()
|
|
||||||
for filterName, filterFunc := range filters {
|
|
||||||
pongo2.RegisterFilter(filterName, filterFunc)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
@@ -234,12 +271,6 @@ func (s *DjangoEngine) loadAssets() error {
|
|||||||
set := pongo2.NewSet("", &tDjangoAssetLoader{baseDir: s.directory, assetGet: s.assetFn})
|
set := pongo2.NewSet("", &tDjangoAssetLoader{baseDir: s.directory, assetGet: s.assetFn})
|
||||||
set.Globals = getPongoContext(s.globals)
|
set.Globals = getPongoContext(s.globals)
|
||||||
|
|
||||||
// set the filters
|
|
||||||
filters := s.convertFilters()
|
|
||||||
for filterName, filterFunc := range filters {
|
|
||||||
pongo2.RegisterFilter(filterName, filterFunc)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(virtualDirectory) > 0 {
|
if len(virtualDirectory) > 0 {
|
||||||
if virtualDirectory[0] == '.' { // first check for .wrong
|
if virtualDirectory[0] == '.' { // first check for .wrong
|
||||||
virtualDirectory = virtualDirectory[1:]
|
virtualDirectory = virtualDirectory[1:]
|
||||||
|
|||||||
Reference in New Issue
Block a user