parent
8d4981fcb8
commit
bd7184d5df
|
@ -24,7 +24,7 @@ type editor struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// newEditor gets the editor based on a FileInfo struct
|
// newEditor gets the editor based on a FileInfo struct
|
||||||
func newEditor(r *http.Request, i *file) (*editor, error) {
|
func newEditor(r *http.Request, i *fileInfo) (*editor, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Create a new editor variable and set the mode
|
// Create a new editor variable and set the mode
|
||||||
|
|
22
file.go
22
file.go
|
@ -13,8 +13,8 @@ import (
|
||||||
humanize "github.com/dustin/go-humanize"
|
humanize "github.com/dustin/go-humanize"
|
||||||
)
|
)
|
||||||
|
|
||||||
// file contains the information about a particular file or directory.
|
// fileInfo contains the information about a particular file or directory.
|
||||||
type file struct {
|
type fileInfo struct {
|
||||||
Name string
|
Name string
|
||||||
Size int64
|
Size int64
|
||||||
URL string
|
URL string
|
||||||
|
@ -30,11 +30,11 @@ type file struct {
|
||||||
UserAllowed bool // Indicates if the user has enough permissions
|
UserAllowed bool // Indicates if the user has enough permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFile retrieves the file information and the error, if there is any.
|
// getFileInfo retrieves the file information and the error, if there is any.
|
||||||
func getFile(url *url.URL, c *Config, u *User) (*file, error) {
|
func getFileInfo(url *url.URL, c *FileManager, u *User) (*fileInfo, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
i := &file{URL: c.PrefixURL + url.Path}
|
i := &fileInfo{URL: c.PrefixURL + url.Path}
|
||||||
i.VirtualPath = strings.Replace(url.Path, c.BaseURL, "", 1)
|
i.VirtualPath = strings.Replace(url.Path, c.BaseURL, "", 1)
|
||||||
i.VirtualPath = strings.TrimPrefix(i.VirtualPath, "/")
|
i.VirtualPath = strings.TrimPrefix(i.VirtualPath, "/")
|
||||||
i.VirtualPath = "/" + i.VirtualPath
|
i.VirtualPath = "/" + i.VirtualPath
|
||||||
|
@ -75,7 +75,7 @@ var textExtensions = [...]string{
|
||||||
|
|
||||||
// RetrieveFileType obtains the mimetype and a simplified internal Type
|
// RetrieveFileType obtains the mimetype and a simplified internal Type
|
||||||
// using the first 512 bytes from the file.
|
// using the first 512 bytes from the file.
|
||||||
func (i *file) RetrieveFileType() error {
|
func (i fileInfo) RetrieveFileType() error {
|
||||||
i.Mimetype = mime.TypeByExtension(i.Extension)
|
i.Mimetype = mime.TypeByExtension(i.Extension)
|
||||||
|
|
||||||
if i.Mimetype == "" {
|
if i.Mimetype == "" {
|
||||||
|
@ -126,7 +126,7 @@ func (i *file) RetrieveFileType() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads the file.
|
// Reads the file.
|
||||||
func (i *file) Read() error {
|
func (i fileInfo) Read() error {
|
||||||
if len(i.Content) != 0 {
|
if len(i.Content) != 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -140,22 +140,22 @@ func (i *file) Read() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// StringifyContent returns the string version of Raw
|
// StringifyContent returns the string version of Raw
|
||||||
func (i file) StringifyContent() string {
|
func (i fileInfo) StringifyContent() string {
|
||||||
return string(i.Content)
|
return string(i.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HumanSize returns the size of the file as a human-readable string
|
// HumanSize returns the size of the file as a human-readable string
|
||||||
// in IEC format (i.e. power of 2 or base 1024).
|
// in IEC format (i.e. power of 2 or base 1024).
|
||||||
func (i file) HumanSize() string {
|
func (i fileInfo) HumanSize() string {
|
||||||
return humanize.IBytes(uint64(i.Size))
|
return humanize.IBytes(uint64(i.Size))
|
||||||
}
|
}
|
||||||
|
|
||||||
// HumanModTime returns the modified time of the file as a human-readable string.
|
// HumanModTime returns the modified time of the file as a human-readable string.
|
||||||
func (i file) HumanModTime(format string) string {
|
func (i fileInfo) HumanModTime(format string) string {
|
||||||
return i.ModTime.Format(format)
|
return i.ModTime.Format(format)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanBeEdited checks if the extension of a file is supported by the editor
|
// CanBeEdited checks if the extension of a file is supported by the editor
|
||||||
func (i file) CanBeEdited() bool {
|
func (i fileInfo) CanBeEdited() bool {
|
||||||
return i.Type == "text"
|
return i.Type == "text"
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"golang.org/x/net/webdav"
|
"golang.org/x/net/webdav"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is a configuration for browsing in a particular path.
|
// FileManager is a configuration for browsing in a particular path.
|
||||||
type Config struct {
|
type FileManager struct {
|
||||||
*User
|
*User
|
||||||
PrefixURL string // A part of the URL that is stripped from the http.Request
|
PrefixURL string // A part of the URL that is stripped from the http.Request
|
||||||
BaseURL string // The base URL of FileManager interface
|
BaseURL string // The base URL of FileManager interface
|
||||||
|
@ -27,8 +27,8 @@ type Config struct {
|
||||||
|
|
||||||
// New creates a new FileManager object with the default settings
|
// New creates a new FileManager object with the default settings
|
||||||
// for a certain scope.
|
// for a certain scope.
|
||||||
func New(scope string) *Config {
|
func New(scope string) *FileManager {
|
||||||
cfg := &Config{
|
fm := &FileManager{
|
||||||
User: &User{
|
User: &User{
|
||||||
Scope: scope,
|
Scope: scope,
|
||||||
FileSystem: webdav.Dir(scope),
|
FileSystem: webdav.Dir(scope),
|
||||||
|
@ -46,26 +46,26 @@ func New(scope string) *Config {
|
||||||
BaseURL: "",
|
BaseURL: "",
|
||||||
PrefixURL: "",
|
PrefixURL: "",
|
||||||
WebDavURL: "/webdav",
|
WebDavURL: "/webdav",
|
||||||
BeforeSave: func(r *http.Request, c *Config, u *User) error { return nil },
|
BeforeSave: func(r *http.Request, c *FileManager, u *User) error { return nil },
|
||||||
AfterSave: func(r *http.Request, c *Config, u *User) error { return nil },
|
AfterSave: func(r *http.Request, c *FileManager, u *User) error { return nil },
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Handler = &webdav.Handler{
|
fm.Handler = &webdav.Handler{
|
||||||
Prefix: cfg.WebDavURL,
|
Prefix: fm.WebDavURL,
|
||||||
FileSystem: cfg.FileSystem,
|
FileSystem: fm.FileSystem,
|
||||||
LockSystem: webdav.NewMemLS(),
|
LockSystem: webdav.NewMemLS(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg
|
return fm
|
||||||
}
|
}
|
||||||
|
|
||||||
// AbsoluteURL ...
|
// AbsoluteURL ...
|
||||||
func (c Config) AbsoluteURL() string {
|
func (c FileManager) AbsoluteURL() string {
|
||||||
return c.PrefixURL + c.BaseURL
|
return c.PrefixURL + c.BaseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// AbsoluteWebdavURL ...
|
// AbsoluteWebdavURL ...
|
||||||
func (c Config) AbsoluteWebdavURL() string {
|
func (c FileManager) AbsoluteWebdavURL() string {
|
||||||
return c.PrefixURL + c.WebDavURL
|
return c.PrefixURL + c.WebDavURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,4 +113,4 @@ func (u User) Allowed(url string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command is a user-defined command that is executed in some moments.
|
// Command is a user-defined command that is executed in some moments.
|
||||||
type Command func(r *http.Request, c *Config, u *User) error
|
type Command func(r *http.Request, c *FileManager, u *User) error
|
||||||
|
|
6
http.go
6
http.go
|
@ -11,9 +11,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServeHTTP starts FileManager.
|
// ServeHTTP starts FileManager.
|
||||||
func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
func (c *FileManager) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
var (
|
var (
|
||||||
fi *file
|
fi *fileInfo
|
||||||
user *User
|
user *User
|
||||||
code int
|
code int
|
||||||
err error
|
err error
|
||||||
|
@ -127,7 +127,7 @@ func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
||||||
|
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
// Gets the information of the directory/file
|
// Gets the information of the directory/file
|
||||||
fi, err = getFile(r.URL, c, user)
|
fi, err = getFileInfo(r.URL, c, user)
|
||||||
code = errorToHTTPCode(err, false)
|
code = errorToHTTPCode(err, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// checksum calculates the hash of a filemanager. Supports MD5, SHA1, SHA256 and SHA512.
|
// checksum calculates the hash of a filemanager. Supports MD5, SHA1, SHA256 and SHA512.
|
||||||
func (c *Config) checksum(w http.ResponseWriter, r *http.Request, i *file) (int, error) {
|
func (c *FileManager) checksum(w http.ResponseWriter, r *http.Request, i *fileInfo) (int, error) {
|
||||||
query := r.URL.Query().Get("checksum")
|
query := r.URL.Query().Get("checksum")
|
||||||
|
|
||||||
file, err := os.Open(i.Path)
|
file, err := os.Open(i.Path)
|
||||||
|
|
|
@ -22,7 +22,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
// command handles the requests for VCS related commands: git, svn and mercurial
|
// command handles the requests for VCS related commands: git, svn and mercurial
|
||||||
func (c *Config) command(w http.ResponseWriter, r *http.Request, u *User) (int, error) {
|
func (c *FileManager) command(w http.ResponseWriter, r *http.Request, u *User) (int, error) {
|
||||||
// Upgrades the connection to a websocket and checks for errors.
|
// Upgrades the connection to a websocket and checks for errors.
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
|
|
||||||
// download creates an archive in one of the supported formats (zip, tar,
|
// download creates an archive in one of the supported formats (zip, tar,
|
||||||
// tar.gz or tar.bz2) and sends it to be downloaded.
|
// tar.gz or tar.bz2) and sends it to be downloaded.
|
||||||
func (c *Config) download(w http.ResponseWriter, r *http.Request, i *file) (int, error) {
|
func (c *FileManager) download(w http.ResponseWriter, r *http.Request, i *fileInfo) (int, error) {
|
||||||
query := r.URL.Query().Get("download")
|
query := r.URL.Query().Get("download")
|
||||||
|
|
||||||
if !i.IsDir {
|
if !i.IsDir {
|
||||||
|
|
|
@ -10,11 +10,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// serveListing presents the user with a listage of a directory folder.
|
// serveListing presents the user with a listage of a directory folder.
|
||||||
func (c *Config) serveListing(w http.ResponseWriter, r *http.Request, u *User, i *file) (int, error) {
|
func (c *FileManager) serveListing(w http.ResponseWriter, r *http.Request, u *User, i *fileInfo) (int, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Loads the content of the directory
|
// Loads the content of the directory
|
||||||
listing, err := GetListing(u, i.VirtualPath, c.PrefixURL+r.URL.Path)
|
listing, err := getListing(u, i.VirtualPath, c.PrefixURL+r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorToHTTPCode(err, true), err
|
return errorToHTTPCode(err, true), err
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// preProccessPUT is used to update a file that was edited
|
// preProccessPUT is used to update a file that was edited
|
||||||
func (c *Config) preProccessPUT(w http.ResponseWriter, r *http.Request, u *User) (err error) {
|
func (c *FileManager) preProccessPUT(w http.ResponseWriter, r *http.Request, u *User) (err error) {
|
||||||
var (
|
var (
|
||||||
data = map[string]interface{}{}
|
data = map[string]interface{}{}
|
||||||
file []byte
|
file []byte
|
||||||
|
|
|
@ -43,7 +43,7 @@ func parseSearch(value string) *searchOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
// search ...
|
// search ...
|
||||||
func (c *Config) search(w http.ResponseWriter, r *http.Request, u *User) (int, error) {
|
func (c *FileManager) search(w http.ResponseWriter, r *http.Request, u *User) (int, error) {
|
||||||
// Upgrades the connection to a websocket and checks for errors.
|
// Upgrades the connection to a websocket and checks for errors.
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
// serveSingle serves a single file in an editor (if it is editable), shows the
|
// serveSingle serves a single file in an editor (if it is editable), shows the
|
||||||
// plain file, or downloads it if it can't be shown.
|
// plain file, or downloads it if it can't be shown.
|
||||||
func (c *Config) serveSingle(w http.ResponseWriter, r *http.Request, u *User, i *file) (int, error) {
|
func (c *FileManager) serveSingle(w http.ResponseWriter, r *http.Request, u *User, i *fileInfo) (int, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if err = i.RetrieveFileType(); err != nil {
|
if err = i.RetrieveFileType(); err != nil {
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
package filemanager
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// errorToHTTPCode converts errors to HTTP Status Code.
|
|
||||||
func errorToHTTPCode(err error, gone bool) int {
|
|
||||||
switch {
|
|
||||||
case os.IsPermission(err):
|
|
||||||
return http.StatusForbidden
|
|
||||||
case os.IsNotExist(err):
|
|
||||||
if !gone {
|
|
||||||
return http.StatusNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.StatusGone
|
|
||||||
case os.IsExist(err):
|
|
||||||
return http.StatusGone
|
|
||||||
default:
|
|
||||||
return http.StatusInternalServerError
|
|
||||||
}
|
|
||||||
}
|
|
26
listing.go
26
listing.go
|
@ -11,14 +11,14 @@ import (
|
||||||
"github.com/mholt/caddy/caddyhttp/httpserver"
|
"github.com/mholt/caddy/caddyhttp/httpserver"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A Listing is the context used to fill out a template.
|
// A listing is the context used to fill out a template.
|
||||||
type Listing struct {
|
type listing struct {
|
||||||
// The name of the directory (the last element of the path)
|
// The name of the directory (the last element of the path)
|
||||||
Name string
|
Name string
|
||||||
// The full path of the request relatively to a File System
|
// The full path of the request relatively to a File System
|
||||||
Path string
|
Path string
|
||||||
// The items (files and folders) in the path
|
// The items (files and folders) in the path
|
||||||
Items []file
|
Items []fileInfo
|
||||||
// The number of directories in the listing
|
// The number of directories in the listing
|
||||||
NumDirs int
|
NumDirs int
|
||||||
// The number of files (items that aren't directories) in the listing
|
// The number of files (items that aren't directories) in the listing
|
||||||
|
@ -32,8 +32,8 @@ type Listing struct {
|
||||||
httpserver.Context `json:"-"`
|
httpserver.Context `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetListing gets the information about a specific directory and its files.
|
// getListing gets the information about a specific directory and its files.
|
||||||
func GetListing(u *User, filePath string, baseURL string) (*Listing, error) {
|
func getListing(u *User, filePath string, baseURL string) (*listing, error) {
|
||||||
// Gets the directory information using the Virtual File System of
|
// Gets the directory information using the Virtual File System of
|
||||||
// the user configuration.
|
// the user configuration.
|
||||||
file, err := u.FileSystem.OpenFile(context.TODO(), filePath, os.O_RDONLY, 0)
|
file, err := u.FileSystem.OpenFile(context.TODO(), filePath, os.O_RDONLY, 0)
|
||||||
|
@ -49,7 +49,7 @@ func GetListing(u *User, filePath string, baseURL string) (*Listing, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
fileinfos []*file
|
fileinfos []fileInfo
|
||||||
dirCount, fileCount int
|
dirCount, fileCount int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ func GetListing(u *User, filePath string, baseURL string) (*Listing, error) {
|
||||||
// Absolute URL
|
// Absolute URL
|
||||||
url := url.URL{Path: baseURL + name}
|
url := url.URL{Path: baseURL + name}
|
||||||
|
|
||||||
i := &file{
|
i := fileInfo{
|
||||||
Name: f.Name(),
|
Name: f.Name(),
|
||||||
Size: f.Size(),
|
Size: f.Size(),
|
||||||
ModTime: f.ModTime(),
|
ModTime: f.ModTime(),
|
||||||
|
@ -85,7 +85,7 @@ func GetListing(u *User, filePath string, baseURL string) (*Listing, error) {
|
||||||
fileinfos = append(fileinfos, i)
|
fileinfos = append(fileinfos, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Listing{
|
return &listing{
|
||||||
Name: path.Base(filePath),
|
Name: path.Base(filePath),
|
||||||
Path: filePath,
|
Path: filePath,
|
||||||
Items: fileinfos,
|
Items: fileinfos,
|
||||||
|
@ -95,7 +95,7 @@ func GetListing(u *User, filePath string, baseURL string) (*Listing, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplySort applies the sort order using .Order and .Sort
|
// ApplySort applies the sort order using .Order and .Sort
|
||||||
func (l Listing) ApplySort() {
|
func (l listing) ApplySort() {
|
||||||
// Check '.Order' to know how to sort
|
// Check '.Order' to know how to sort
|
||||||
if l.Order == "desc" {
|
if l.Order == "desc" {
|
||||||
switch l.Sort {
|
switch l.Sort {
|
||||||
|
@ -124,10 +124,10 @@ func (l Listing) ApplySort() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement sorting for Listing
|
// Implement sorting for listing
|
||||||
type byName Listing
|
type byName listing
|
||||||
type bySize Listing
|
type bySize listing
|
||||||
type byTime Listing
|
type byTime listing
|
||||||
|
|
||||||
// By Name
|
// By Name
|
||||||
func (l byName) Len() int {
|
func (l byName) Len() int {
|
||||||
|
|
11
page.go
11
page.go
|
@ -11,6 +11,15 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Create the functions map, then the template, check for erros and
|
||||||
|
// execute the template if there aren't errors
|
||||||
|
var functionMap = template.FuncMap{
|
||||||
|
"Defined": defined,
|
||||||
|
"CSS": css,
|
||||||
|
"Marshal": marshal,
|
||||||
|
"EncodeBase64": encodeBase64,
|
||||||
|
}
|
||||||
|
|
||||||
// page contains the informations and functions needed to show the Page
|
// page contains the informations and functions needed to show the Page
|
||||||
type page struct {
|
type page struct {
|
||||||
Info *pageInfo
|
Info *pageInfo
|
||||||
|
@ -23,7 +32,7 @@ type pageInfo struct {
|
||||||
Path string
|
Path string
|
||||||
IsDir bool
|
IsDir bool
|
||||||
User *User
|
User *User
|
||||||
Config *Config
|
Config *FileManager
|
||||||
Data interface{}
|
Data interface{}
|
||||||
Editor bool
|
Editor bool
|
||||||
Display string
|
Display string
|
||||||
|
|
|
@ -5,18 +5,11 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create the functions map, then the template, check for erros and
|
|
||||||
// execute the template if there aren't errors
|
|
||||||
var functionMap = template.FuncMap{
|
|
||||||
"Defined": defined,
|
|
||||||
"CSS": css,
|
|
||||||
"Marshal": marshal,
|
|
||||||
"EncodeBase64": encodeBase64,
|
|
||||||
}
|
|
||||||
|
|
||||||
// defined checks if variable is defined in a struct
|
// defined checks if variable is defined in a struct
|
||||||
func defined(data interface{}, field string) bool {
|
func defined(data interface{}, field string) bool {
|
||||||
t := reflect.Indirect(reflect.ValueOf(data)).Type()
|
t := reflect.Indirect(reflect.ValueOf(data)).Type()
|
||||||
|
@ -45,3 +38,21 @@ func marshal(v interface{}) template.JS {
|
||||||
func encodeBase64(s string) string {
|
func encodeBase64(s string) string {
|
||||||
return base64.StdEncoding.EncodeToString([]byte(s))
|
return base64.StdEncoding.EncodeToString([]byte(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// errorToHTTPCode converts errors to HTTP Status Code.
|
||||||
|
func errorToHTTPCode(err error, gone bool) int {
|
||||||
|
switch {
|
||||||
|
case os.IsPermission(err):
|
||||||
|
return http.StatusForbidden
|
||||||
|
case os.IsNotExist(err):
|
||||||
|
if !gone {
|
||||||
|
return http.StatusNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.StatusGone
|
||||||
|
case os.IsExist(err):
|
||||||
|
return http.StatusGone
|
||||||
|
default:
|
||||||
|
return http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue