mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-22 07:30:25 +00:00
add a basic web interface
The builtin web interface allows to manage users and connections
This commit is contained in:
parent
bb0338870a
commit
afd312f26a
56 changed files with 7060 additions and 327 deletions
17
README.md
17
README.md
|
@ -163,6 +163,8 @@ The `sftpgo` configuration file contains the following sections:
|
||||||
- **"httpd"**, the configuration for the HTTP server used to serve REST API
|
- **"httpd"**, the configuration for the HTTP server used to serve REST API
|
||||||
- `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 8080
|
- `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 8080
|
||||||
- `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1"
|
- `bind_address`, string. Leave blank to listen on all available network interfaces. Default: "127.0.0.1"
|
||||||
|
- `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
|
||||||
|
- `static_files_path`, string. Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir
|
||||||
|
|
||||||
Here is a full example showing the default config in JSON format:
|
Here is a full example showing the default config in JSON format:
|
||||||
|
|
||||||
|
@ -199,7 +201,9 @@ Here is a full example showing the default config in JSON format:
|
||||||
},
|
},
|
||||||
"httpd": {
|
"httpd": {
|
||||||
"bind_port": 8080,
|
"bind_port": 8080,
|
||||||
"bind_address": "127.0.0.1"
|
"bind_address": "127.0.0.1",
|
||||||
|
"templates_path": "templates",
|
||||||
|
"static_files_path": "static"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -299,7 +303,7 @@ SFTPGo exposes REST API to manage users and quota and to get real time reports f
|
||||||
|
|
||||||
If quota tracking is enabled in `sftpgo` configuration file, then the used size and number of files are updated each time a file is added/removed. If files are added/removed not using SFTP or if you change `track_quota` from `2` to `1`, you can rescan the user home dir and update the used quota using the REST API.
|
If quota tracking is enabled in `sftpgo` configuration file, then the used size and number of files are updated each time a file is added/removed. If files are added/removed not using SFTP or if you change `track_quota` from `2` to `1`, you can rescan the user home dir and update the used quota using the REST API.
|
||||||
|
|
||||||
REST API is designed to run on localhost or on a trusted network, if you need HTTPS or authentication you can setup a reverse proxy using an HTTP Server such as Apache or NGNIX.
|
REST API is designed to run on localhost or on a trusted network, if you need HTTPS and/or authentication you can setup a reverse proxy using an HTTP Server such as Apache or NGNIX.
|
||||||
|
|
||||||
For example you can keep SFTPGo listening on localhost and expose it externally configuring a reverse proxy using Apache HTTP Server this way:
|
For example you can keep SFTPGo listening on localhost and expose it externally configuring a reverse proxy using Apache HTTP Server this way:
|
||||||
|
|
||||||
|
@ -346,6 +350,15 @@ Several counters and gauges are available, for example:
|
||||||
|
|
||||||
Please check the `/metrics` page for more details.
|
Please check the `/metrics` page for more details.
|
||||||
|
|
||||||
|
## Web Admin
|
||||||
|
|
||||||
|
You can easily build your own interface using the exposed REST API, anyway SFTPGo provides also a very basic builtin web interface that allows to manage users and connections.
|
||||||
|
With the default `httpd` configuration, the web admin is available at the following URL:
|
||||||
|
|
||||||
|
[http://127.0.0.1:8080/web](http://127.0.0.1:8080/web)
|
||||||
|
|
||||||
|
If you need HTTPS and/or authentication you can setup a reverse proxy as explained for the REST API.
|
||||||
|
|
||||||
## Logs
|
## Logs
|
||||||
|
|
||||||
Inside the log file each line is a JSON struct, each struct has a `sender` fields that identify the log type.
|
Inside the log file each line is a JSON struct, each struct has a `sender` fields that identify the log type.
|
||||||
|
|
78
api/api.go
78
api/api.go
|
@ -1,78 +0,0 @@
|
||||||
// Package api implements REST API for sftpgo.
|
|
||||||
// REST API allows to manage users and quota and to get real time reports for the active connections
|
|
||||||
// with possibility of forcibly closing a connection.
|
|
||||||
// The OpenAPI 3 schema for the exposed API can be found inside the source tree:
|
|
||||||
// https://github.com/drakkan/sftpgo/tree/master/api/schema/openapi.yaml
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/dataprovider"
|
|
||||||
"github.com/go-chi/chi"
|
|
||||||
"github.com/go-chi/render"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
logSender = "api"
|
|
||||||
activeConnectionsPath = "/api/v1/connection"
|
|
||||||
quotaScanPath = "/api/v1/quota_scan"
|
|
||||||
userPath = "/api/v1/user"
|
|
||||||
versionPath = "/api/v1/version"
|
|
||||||
metricsPath = "/metrics"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
router *chi.Mux
|
|
||||||
dataProvider dataprovider.Provider
|
|
||||||
)
|
|
||||||
|
|
||||||
// HTTPDConf httpd daemon configuration
|
|
||||||
type HTTPDConf struct {
|
|
||||||
// The port used for serving HTTP requests. 0 disable the HTTP server. Default: 8080
|
|
||||||
BindPort int `json:"bind_port" mapstructure:"bind_port"`
|
|
||||||
// The address to listen on. A blank value means listen on all available network interfaces. Default: "127.0.0.1"
|
|
||||||
BindAddress string `json:"bind_address" mapstructure:"bind_address"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiResponse struct {
|
|
||||||
Error string `json:"error"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
HTTPStatus int `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
initializeRouter()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDataProvider sets the data provider to use to fetch the data about users
|
|
||||||
func SetDataProvider(provider dataprovider.Provider) {
|
|
||||||
dataProvider = provider
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {
|
|
||||||
var errorString string
|
|
||||||
if err != nil {
|
|
||||||
errorString = err.Error()
|
|
||||||
}
|
|
||||||
resp := apiResponse{
|
|
||||||
Error: errorString,
|
|
||||||
Message: message,
|
|
||||||
HTTPStatus: code,
|
|
||||||
}
|
|
||||||
if code != http.StatusOK {
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
w.WriteHeader(code)
|
|
||||||
}
|
|
||||||
render.JSON(w, r, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRespStatus(err error) int {
|
|
||||||
if _, ok := err.(*dataprovider.ValidationError); ok {
|
|
||||||
return http.StatusBadRequest
|
|
||||||
}
|
|
||||||
if _, ok := err.(*dataprovider.MethodDisabledError); ok {
|
|
||||||
return http.StatusForbidden
|
|
||||||
}
|
|
||||||
return http.StatusInternalServerError
|
|
||||||
}
|
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/api"
|
|
||||||
"github.com/drakkan/sftpgo/dataprovider"
|
"github.com/drakkan/sftpgo/dataprovider"
|
||||||
|
"github.com/drakkan/sftpgo/httpd"
|
||||||
"github.com/drakkan/sftpgo/logger"
|
"github.com/drakkan/sftpgo/logger"
|
||||||
"github.com/drakkan/sftpgo/sftpd"
|
"github.com/drakkan/sftpgo/sftpd"
|
||||||
"github.com/drakkan/sftpgo/utils"
|
"github.com/drakkan/sftpgo/utils"
|
||||||
|
@ -35,7 +35,7 @@ var (
|
||||||
type globalConfig struct {
|
type globalConfig struct {
|
||||||
SFTPD sftpd.Configuration `json:"sftpd" mapstructure:"sftpd"`
|
SFTPD sftpd.Configuration `json:"sftpd" mapstructure:"sftpd"`
|
||||||
ProviderConf dataprovider.Config `json:"data_provider" mapstructure:"data_provider"`
|
ProviderConf dataprovider.Config `json:"data_provider" mapstructure:"data_provider"`
|
||||||
HTTPDConfig api.HTTPDConf `json:"httpd" mapstructure:"httpd"`
|
HTTPDConfig httpd.Conf `json:"httpd" mapstructure:"httpd"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -76,9 +76,11 @@ func init() {
|
||||||
PoolSize: 0,
|
PoolSize: 0,
|
||||||
UsersBaseDir: "",
|
UsersBaseDir: "",
|
||||||
},
|
},
|
||||||
HTTPDConfig: api.HTTPDConf{
|
HTTPDConfig: httpd.Conf{
|
||||||
BindPort: 8080,
|
BindPort: 8080,
|
||||||
BindAddress: "127.0.0.1",
|
BindAddress: "127.0.0.1",
|
||||||
|
TemplatesPath: "templates",
|
||||||
|
StaticFilesPath: "static",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +98,7 @@ func GetSFTPDConfig() sftpd.Configuration {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHTTPDConfig returns the configuration for the HTTP server
|
// GetHTTPDConfig returns the configuration for the HTTP server
|
||||||
func GetHTTPDConfig() api.HTTPDConf {
|
func GetHTTPDConfig() httpd.Conf {
|
||||||
return globalConf.HTTPDConfig
|
return globalConf.HTTPDConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/api"
|
|
||||||
"github.com/drakkan/sftpgo/config"
|
"github.com/drakkan/sftpgo/config"
|
||||||
"github.com/drakkan/sftpgo/dataprovider"
|
"github.com/drakkan/sftpgo/dataprovider"
|
||||||
|
"github.com/drakkan/sftpgo/httpd"
|
||||||
"github.com/drakkan/sftpgo/sftpd"
|
"github.com/drakkan/sftpgo/sftpd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ func TestLoadConfigTest(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error loading config")
|
t.Errorf("error loading config")
|
||||||
}
|
}
|
||||||
emptyHTTPDConf := api.HTTPDConf{}
|
emptyHTTPDConf := httpd.Conf{}
|
||||||
if config.GetHTTPDConfig() == emptyHTTPDConf {
|
if config.GetHTTPDConfig() == emptyHTTPDConf {
|
||||||
t.Errorf("error loading httpd conf")
|
t.Errorf("error loading httpd conf")
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,11 +51,12 @@ const (
|
||||||
var (
|
var (
|
||||||
// SupportedProviders data provider configured in the sftpgo.conf file must match of these strings
|
// SupportedProviders data provider configured in the sftpgo.conf file must match of these strings
|
||||||
SupportedProviders = []string{SQLiteDataProviderName, PGSQLDataProviderName, MySQLDataProviderName, BoltDataProviderName}
|
SupportedProviders = []string{SQLiteDataProviderName, PGSQLDataProviderName, MySQLDataProviderName, BoltDataProviderName}
|
||||||
config Config
|
// ValidPerms list that contains all the valid permissions for an user
|
||||||
provider Provider
|
ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermRename, PermDelete,
|
||||||
sqlPlaceholders []string
|
PermCreateDirs, PermCreateSymlinks}
|
||||||
validPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermDelete, PermRename,
|
config Config
|
||||||
PermCreateDirs, PermCreateSymlinks, PermOverwrite}
|
provider Provider
|
||||||
|
sqlPlaceholders []string
|
||||||
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,
|
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,
|
||||||
pbkdf2SHA512Prefix, sha512cryptPwdPrefix}
|
pbkdf2SHA512Prefix, sha512cryptPwdPrefix}
|
||||||
pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix}
|
pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix}
|
||||||
|
@ -276,13 +277,25 @@ func buildUserHomeDir(user *User) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validatePermissions(user *User) error {
|
||||||
|
for _, p := range user.Permissions {
|
||||||
|
if !utils.IsStringInSlice(p, ValidPerms) {
|
||||||
|
return &ValidationError{err: fmt.Sprintf("Invalid permission: %v", p)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if utils.IsStringInSlice(PermAny, user.Permissions) {
|
||||||
|
user.Permissions = []string{PermAny}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func validateUser(user *User) error {
|
func validateUser(user *User) error {
|
||||||
buildUserHomeDir(user)
|
buildUserHomeDir(user)
|
||||||
if len(user.Username) == 0 || len(user.HomeDir) == 0 {
|
if len(user.Username) == 0 || len(user.HomeDir) == 0 {
|
||||||
return &ValidationError{err: "Mandatory parameters missing"}
|
return &ValidationError{err: "Mandatory parameters missing"}
|
||||||
}
|
}
|
||||||
if len(user.Password) == 0 && len(user.PublicKeys) == 0 {
|
if len(user.Password) == 0 && len(user.PublicKeys) == 0 {
|
||||||
return &ValidationError{err: "Please set password or at least a public_key"}
|
return &ValidationError{err: "Please set a password or at least a public_key"}
|
||||||
}
|
}
|
||||||
if len(user.Permissions) == 0 {
|
if len(user.Permissions) == 0 {
|
||||||
return &ValidationError{err: "Please grant some permissions to this user"}
|
return &ValidationError{err: "Please grant some permissions to this user"}
|
||||||
|
@ -290,10 +303,8 @@ func validateUser(user *User) error {
|
||||||
if !filepath.IsAbs(user.HomeDir) {
|
if !filepath.IsAbs(user.HomeDir) {
|
||||||
return &ValidationError{err: fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir)}
|
return &ValidationError{err: fmt.Sprintf("home_dir must be an absolute path, actual value: %v", user.HomeDir)}
|
||||||
}
|
}
|
||||||
for _, p := range user.Permissions {
|
if err := validatePermissions(user); err != nil {
|
||||||
if !utils.IsStringInSlice(p, validPerms) {
|
return err
|
||||||
return &ValidationError{err: fmt.Sprintf("Invalid permission: %v", p)}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(user.Password) > 0 && !utils.IsStringPrefixInSlice(user.Password, hashPwdPrefixes) {
|
if len(user.Password) > 0 && !utils.IsStringPrefixInSlice(user.Password, hashPwdPrefixes) {
|
||||||
pwd, err := argon2id.CreateHash(user.Password, argon2id.DefaultParams)
|
pwd, err := argon2id.CreateHash(user.Password, argon2id.DefaultParams)
|
||||||
|
|
|
@ -2,7 +2,9 @@ package dataprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/utils"
|
"github.com/drakkan/sftpgo/utils"
|
||||||
)
|
)
|
||||||
|
@ -123,3 +125,67 @@ func (u *User) GetRelativePath(path string) string {
|
||||||
}
|
}
|
||||||
return "/" + filepath.ToSlash(rel)
|
return "/" + filepath.ToSlash(rel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetQuotaSummary returns used quota and limits if defined
|
||||||
|
func (u *User) GetQuotaSummary() string {
|
||||||
|
var result string
|
||||||
|
result = "Files: " + strconv.Itoa(u.UsedQuotaFiles)
|
||||||
|
if u.QuotaFiles > 0 {
|
||||||
|
result += "/" + strconv.Itoa(u.QuotaFiles)
|
||||||
|
}
|
||||||
|
if u.UsedQuotaSize > 0 || u.QuotaSize > 0 {
|
||||||
|
result += ". Size: " + utils.ByteCountSI(u.UsedQuotaSize)
|
||||||
|
if u.QuotaSize > 0 {
|
||||||
|
result += "/" + utils.ByteCountSI(u.QuotaSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPermissionsAsString returns the user's permissions as comma separated string
|
||||||
|
func (u *User) GetPermissionsAsString() string {
|
||||||
|
var result string
|
||||||
|
for _, p := range u.Permissions {
|
||||||
|
if len(result) > 0 {
|
||||||
|
result += ", "
|
||||||
|
}
|
||||||
|
result += p
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBandwidthAsString returns bandwidth limits if defines
|
||||||
|
func (u *User) GetBandwidthAsString() string {
|
||||||
|
result := "Download: "
|
||||||
|
if u.DownloadBandwidth > 0 {
|
||||||
|
result += utils.ByteCountSI(u.DownloadBandwidth*1000) + "/s."
|
||||||
|
} else {
|
||||||
|
result += "ulimited."
|
||||||
|
}
|
||||||
|
result += " Upload: "
|
||||||
|
if u.UploadBandwidth > 0 {
|
||||||
|
result += utils.ByteCountSI(u.UploadBandwidth*1000) + "/s."
|
||||||
|
} else {
|
||||||
|
result += "ulimited."
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInfoString returns user's info as string.
|
||||||
|
// Number of public keys, max sessions, uid and gid are returned
|
||||||
|
func (u *User) GetInfoString() string {
|
||||||
|
var result string
|
||||||
|
if len(u.PublicKeys) > 0 {
|
||||||
|
result += fmt.Sprintf("Public keys: %v ", len(u.PublicKeys))
|
||||||
|
}
|
||||||
|
if u.MaxSessions > 0 {
|
||||||
|
result += fmt.Sprintf("Max sessions: %v ", u.MaxSessions)
|
||||||
|
}
|
||||||
|
if u.UID > 0 {
|
||||||
|
result += fmt.Sprintf("UID: %v ", u.UID)
|
||||||
|
}
|
||||||
|
if u.GID > 0 {
|
||||||
|
result += fmt.Sprintf("GID: %v ", u.GID)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package api
|
package httpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
|
@ -1,4 +1,4 @@
|
||||||
package api
|
package httpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
|
@ -1,4 +1,4 @@
|
||||||
package api
|
package httpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -42,6 +42,33 @@ func buildURLRelativeToBase(paths ...string) string {
|
||||||
return fmt.Sprintf("%s/%s", strings.TrimRight(httpBaseURL, "/"), strings.TrimLeft(p, "/"))
|
return fmt.Sprintf("%s/%s", strings.TrimRight(httpBaseURL, "/"), strings.TrimLeft(p, "/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {
|
||||||
|
var errorString string
|
||||||
|
if err != nil {
|
||||||
|
errorString = err.Error()
|
||||||
|
}
|
||||||
|
resp := apiResponse{
|
||||||
|
Error: errorString,
|
||||||
|
Message: message,
|
||||||
|
HTTPStatus: code,
|
||||||
|
}
|
||||||
|
if code != http.StatusOK {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.WriteHeader(code)
|
||||||
|
}
|
||||||
|
render.JSON(w, r, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRespStatus(err error) int {
|
||||||
|
if _, ok := err.(*dataprovider.ValidationError); ok {
|
||||||
|
return http.StatusBadRequest
|
||||||
|
}
|
||||||
|
if _, ok := err.(*dataprovider.MethodDisabledError); ok {
|
||||||
|
return http.StatusForbidden
|
||||||
|
}
|
||||||
|
return http.StatusInternalServerError
|
||||||
|
}
|
||||||
|
|
||||||
// AddUser adds a new user and checks the received HTTP Status code against expectedStatusCode.
|
// AddUser adds a new user and checks the received HTTP Status code against expectedStatusCode.
|
||||||
func AddUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User, []byte, error) {
|
func AddUser(user dataprovider.User, expectedStatusCode int) (dataprovider.User, []byte, error) {
|
||||||
var newUser dataprovider.User
|
var newUser dataprovider.User
|
78
httpd/httpd.go
Normal file
78
httpd/httpd.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// Package httpd implements REST API and Web interface for SFTPGo.
|
||||||
|
// REST API allows to manage users and quota and to get real time reports for the active connections
|
||||||
|
// with possibility of forcibly closing a connection.
|
||||||
|
// The OpenAPI 3 schema for the exposed API can be found inside the source tree:
|
||||||
|
// https://github.com/drakkan/sftpgo/tree/master/api/schema/openapi.yaml
|
||||||
|
// A basic Web interface to manage users and connections is provided too
|
||||||
|
package httpd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/dataprovider"
|
||||||
|
"github.com/drakkan/sftpgo/logger"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
logSender = "api"
|
||||||
|
activeConnectionsPath = "/api/v1/connection"
|
||||||
|
quotaScanPath = "/api/v1/quota_scan"
|
||||||
|
userPath = "/api/v1/user"
|
||||||
|
versionPath = "/api/v1/version"
|
||||||
|
metricsPath = "/metrics"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
router *chi.Mux
|
||||||
|
dataProvider dataprovider.Provider
|
||||||
|
)
|
||||||
|
|
||||||
|
// Conf httpd daemon configuration
|
||||||
|
type Conf struct {
|
||||||
|
// The port used for serving HTTP requests. 0 disable the HTTP server. Default: 8080
|
||||||
|
BindPort int `json:"bind_port" mapstructure:"bind_port"`
|
||||||
|
// The address to listen on. A blank value means listen on all available network interfaces. Default: "127.0.0.1"
|
||||||
|
BindAddress string `json:"bind_address" mapstructure:"bind_address"`
|
||||||
|
// Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
|
||||||
|
TemplatesPath string `json:"templates_path" mapstructure:"templates_path"`
|
||||||
|
// Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir
|
||||||
|
StaticFilesPath string `json:"static_files_path" mapstructure:"static_files_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
HTTPStatus int `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDataProvider sets the data provider to use to fetch the data about users
|
||||||
|
func SetDataProvider(provider dataprovider.Provider) {
|
||||||
|
dataProvider = provider
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the HTTP server
|
||||||
|
func (c Conf) Initialize(configDir string) error {
|
||||||
|
logger.Debug(logSender, "", "initializing HTTP server with config %+v", c)
|
||||||
|
staticFilesPath := c.StaticFilesPath
|
||||||
|
if !filepath.IsAbs(staticFilesPath) {
|
||||||
|
staticFilesPath = filepath.Join(configDir, staticFilesPath)
|
||||||
|
}
|
||||||
|
templatesPath := c.TemplatesPath
|
||||||
|
if !filepath.IsAbs(templatesPath) {
|
||||||
|
templatesPath = filepath.Join(configDir, templatesPath)
|
||||||
|
}
|
||||||
|
loadTemplates(templatesPath)
|
||||||
|
initializeRouter(staticFilesPath)
|
||||||
|
httpServer := &http.Server{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", c.BindAddress, c.BindPort),
|
||||||
|
Handler: router,
|
||||||
|
ReadTimeout: 300 * time.Second,
|
||||||
|
WriteTimeout: 300 * time.Second,
|
||||||
|
MaxHeaderBytes: 1 << 20, // 1MB
|
||||||
|
}
|
||||||
|
return httpServer.ListenAndServe()
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package api_test
|
package httpd_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -7,10 +7,12 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -20,11 +22,12 @@ import (
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/api"
|
|
||||||
"github.com/drakkan/sftpgo/config"
|
"github.com/drakkan/sftpgo/config"
|
||||||
"github.com/drakkan/sftpgo/dataprovider"
|
"github.com/drakkan/sftpgo/dataprovider"
|
||||||
|
"github.com/drakkan/sftpgo/httpd"
|
||||||
"github.com/drakkan/sftpgo/logger"
|
"github.com/drakkan/sftpgo/logger"
|
||||||
"github.com/drakkan/sftpgo/sftpd"
|
"github.com/drakkan/sftpgo/sftpd"
|
||||||
|
"github.com/drakkan/sftpgo/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -37,13 +40,18 @@ const (
|
||||||
quotaScanPath = "/api/v1/quota_scan"
|
quotaScanPath = "/api/v1/quota_scan"
|
||||||
versionPath = "/api/v1/version"
|
versionPath = "/api/v1/version"
|
||||||
metricsPath = "/metrics"
|
metricsPath = "/metrics"
|
||||||
|
webBasePath = "/web"
|
||||||
|
webUsersPath = "/web/users"
|
||||||
|
webUserPath = "/web/user"
|
||||||
|
webConnectionsPath = "/web/connections"
|
||||||
configDir = ".."
|
configDir = ".."
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
defaultPerms = []string{dataprovider.PermAny}
|
defaultPerms = []string{dataprovider.PermAny}
|
||||||
homeBasePath string
|
homeBasePath string
|
||||||
testServer *httptest.Server
|
testServer *httptest.Server
|
||||||
|
providerDriverName string
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
|
@ -56,6 +64,7 @@ func TestMain(m *testing.M) {
|
||||||
logger.InitLogger(logfilePath, 5, 1, 28, false, zerolog.DebugLevel)
|
logger.InitLogger(logfilePath, 5, 1, 28, false, zerolog.DebugLevel)
|
||||||
config.LoadConfig(configDir, "")
|
config.LoadConfig(configDir, "")
|
||||||
providerConf := config.GetProviderConf()
|
providerConf := config.GetProviderConf()
|
||||||
|
providerDriverName = providerConf.Driver
|
||||||
|
|
||||||
err := dataprovider.Initialize(providerConf, configDir)
|
err := dataprovider.Initialize(providerConf, configDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -64,40 +73,33 @@ func TestMain(m *testing.M) {
|
||||||
}
|
}
|
||||||
dataProvider := dataprovider.GetProvider()
|
dataProvider := dataprovider.GetProvider()
|
||||||
httpdConf := config.GetHTTPDConfig()
|
httpdConf := config.GetHTTPDConfig()
|
||||||
router := api.GetHTTPRouter()
|
|
||||||
|
|
||||||
httpdConf.BindPort = 8081
|
httpdConf.BindPort = 8081
|
||||||
api.SetBaseURL("http://127.0.0.1:8081")
|
httpd.SetBaseURL("http://127.0.0.1:8081")
|
||||||
|
|
||||||
sftpd.SetDataProvider(dataProvider)
|
sftpd.SetDataProvider(dataProvider)
|
||||||
api.SetDataProvider(dataProvider)
|
httpd.SetDataProvider(dataProvider)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
logger.Debug(logSender, "", "initializing HTTP server with config %+v", httpdConf)
|
go func() {
|
||||||
s := &http.Server{
|
if err := httpdConf.Initialize(configDir); err != nil {
|
||||||
Addr: fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort),
|
logger.Error(logSender, "", "could not start HTTP server: %v", err)
|
||||||
Handler: router,
|
}
|
||||||
ReadTimeout: 300 * time.Second,
|
}()
|
||||||
WriteTimeout: 300 * time.Second,
|
|
||||||
MaxHeaderBytes: 1 << 20, // 1MB
|
|
||||||
}
|
|
||||||
if err := s.ListenAndServe(); err != nil {
|
|
||||||
logger.Error(logSender, "", "could not start HTTP server: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
testServer = httptest.NewServer(api.GetHTTPRouter())
|
|
||||||
defer testServer.Close()
|
|
||||||
|
|
||||||
waitTCPListening(fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort))
|
waitTCPListening(fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort))
|
||||||
|
|
||||||
|
testServer = httptest.NewServer(httpd.GetHTTPRouter())
|
||||||
|
defer testServer.Close()
|
||||||
|
|
||||||
exitCode := m.Run()
|
exitCode := m.Run()
|
||||||
os.Remove(logfilePath)
|
os.Remove(logfilePath)
|
||||||
os.Exit(exitCode)
|
os.Exit(exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasicUserHandling(t *testing.T) {
|
func TestBasicUserHandling(t *testing.T) {
|
||||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -106,18 +108,18 @@ func TestBasicUserHandling(t *testing.T) {
|
||||||
user.QuotaFiles = 2
|
user.QuotaFiles = 2
|
||||||
user.UploadBandwidth = 128
|
user.UploadBandwidth = 128
|
||||||
user.DownloadBandwidth = 64
|
user.DownloadBandwidth = 64
|
||||||
user, _, err = api.UpdateUser(user, http.StatusOK)
|
user, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to update user: %v", err)
|
t.Errorf("unable to update user: %v", err)
|
||||||
}
|
}
|
||||||
users, _, err := api.GetUsers(0, 0, defaultUsername, http.StatusOK)
|
users, _, err := httpd.GetUsers(0, 0, defaultUsername, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to get users: %v", err)
|
t.Errorf("unable to get users: %v", err)
|
||||||
}
|
}
|
||||||
if len(users) != 1 {
|
if len(users) != 1 {
|
||||||
t.Errorf("number of users mismatch, expected: 1, actual: %v", len(users))
|
t.Errorf("number of users mismatch, expected: 1, actual: %v", len(users))
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove: %v", err)
|
t.Errorf("unable to remove: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -127,7 +129,7 @@ func TestAddUserNoCredentials(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
u.Password = ""
|
u.Password = ""
|
||||||
u.PublicKeys = []string{}
|
u.PublicKeys = []string{}
|
||||||
_, _, err := api.AddUser(u, http.StatusBadRequest)
|
_, _, err := httpd.AddUser(u, http.StatusBadRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error adding user with no credentials: %v", err)
|
t.Errorf("unexpected error adding user with no credentials: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -136,7 +138,7 @@ func TestAddUserNoCredentials(t *testing.T) {
|
||||||
func TestAddUserNoUsername(t *testing.T) {
|
func TestAddUserNoUsername(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
u.Username = ""
|
u.Username = ""
|
||||||
_, _, err := api.AddUser(u, http.StatusBadRequest)
|
_, _, err := httpd.AddUser(u, http.StatusBadRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error adding user with no home dir: %v", err)
|
t.Errorf("unexpected error adding user with no home dir: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -145,7 +147,7 @@ func TestAddUserNoUsername(t *testing.T) {
|
||||||
func TestAddUserNoHomeDir(t *testing.T) {
|
func TestAddUserNoHomeDir(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
u.HomeDir = ""
|
u.HomeDir = ""
|
||||||
_, _, err := api.AddUser(u, http.StatusBadRequest)
|
_, _, err := httpd.AddUser(u, http.StatusBadRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error adding user with no home dir: %v", err)
|
t.Errorf("unexpected error adding user with no home dir: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -154,7 +156,7 @@ func TestAddUserNoHomeDir(t *testing.T) {
|
||||||
func TestAddUserInvalidHomeDir(t *testing.T) {
|
func TestAddUserInvalidHomeDir(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
u.HomeDir = "relative_path"
|
u.HomeDir = "relative_path"
|
||||||
_, _, err := api.AddUser(u, http.StatusBadRequest)
|
_, _, err := httpd.AddUser(u, http.StatusBadRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error adding user with invalid home dir: %v", err)
|
t.Errorf("unexpected error adding user with invalid home dir: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -163,7 +165,7 @@ func TestAddUserInvalidHomeDir(t *testing.T) {
|
||||||
func TestAddUserNoPerms(t *testing.T) {
|
func TestAddUserNoPerms(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
u.Permissions = []string{}
|
u.Permissions = []string{}
|
||||||
_, _, err := api.AddUser(u, http.StatusBadRequest)
|
_, _, err := httpd.AddUser(u, http.StatusBadRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error adding user with no perms: %v", err)
|
t.Errorf("unexpected error adding user with no perms: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -172,7 +174,7 @@ func TestAddUserNoPerms(t *testing.T) {
|
||||||
func TestAddUserInvalidPerms(t *testing.T) {
|
func TestAddUserInvalidPerms(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
u.Permissions = []string{"invalidPerm"}
|
u.Permissions = []string{"invalidPerm"}
|
||||||
_, _, err := api.AddUser(u, http.StatusBadRequest)
|
_, _, err := httpd.AddUser(u, http.StatusBadRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error adding user with no perms: %v", err)
|
t.Errorf("unexpected error adding user with no perms: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -183,33 +185,33 @@ func TestUserPublicKey(t *testing.T) {
|
||||||
invalidPubKey := "invalid"
|
invalidPubKey := "invalid"
|
||||||
validPubKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1"
|
validPubKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC03jj0D+djk7pxIf/0OhrxrchJTRZklofJ1NoIu4752Sq02mdXmarMVsqJ1cAjV5LBVy3D1F5U6XW4rppkXeVtd04Pxb09ehtH0pRRPaoHHlALiJt8CoMpbKYMA8b3KXPPriGxgGomvtU2T2RMURSwOZbMtpsugfjYSWenyYX+VORYhylWnSXL961LTyC21ehd6d6QnW9G7E5hYMITMY9TuQZz3bROYzXiTsgN0+g6Hn7exFQp50p45StUMfV/SftCMdCxlxuyGny2CrN/vfjO7xxOo2uv7q1qm10Q46KPWJQv+pgZ/OfL+EDjy07n5QVSKHlbx+2nT4Q0EgOSQaCTYwn3YjtABfIxWwgAFdyj6YlPulCL22qU4MYhDcA6PSBwDdf8hvxBfvsiHdM+JcSHvv8/VeJhk6CmnZxGY0fxBupov27z3yEO8nAg8k+6PaUiW1MSUfuGMF/ktB8LOstXsEPXSszuyXiOv4DaryOXUiSn7bmRqKcEFlJusO6aZP0= nicola@p1"
|
||||||
u.PublicKeys = []string{invalidPubKey}
|
u.PublicKeys = []string{invalidPubKey}
|
||||||
_, _, err := api.AddUser(u, http.StatusBadRequest)
|
_, _, err := httpd.AddUser(u, http.StatusBadRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error adding user with invalid pub key: %v", err)
|
t.Errorf("unexpected error adding user with invalid pub key: %v", err)
|
||||||
}
|
}
|
||||||
u.PublicKeys = []string{validPubKey}
|
u.PublicKeys = []string{validPubKey}
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
user.PublicKeys = []string{validPubKey, invalidPubKey}
|
user.PublicKeys = []string{validPubKey, invalidPubKey}
|
||||||
_, _, err = api.UpdateUser(user, http.StatusBadRequest)
|
_, _, err = httpd.UpdateUser(user, http.StatusBadRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("update user with invalid public key must fail: %v", err)
|
t.Errorf("update user with invalid public key must fail: %v", err)
|
||||||
}
|
}
|
||||||
user.PublicKeys = []string{validPubKey, validPubKey, validPubKey}
|
user.PublicKeys = []string{validPubKey, validPubKey, validPubKey}
|
||||||
_, _, err = api.UpdateUser(user, http.StatusOK)
|
_, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to update user: %v", err)
|
t.Errorf("unable to update user: %v", err)
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove: %v", err)
|
t.Errorf("unable to remove: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateUser(t *testing.T) {
|
func TestUpdateUser(t *testing.T) {
|
||||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -222,18 +224,18 @@ func TestUpdateUser(t *testing.T) {
|
||||||
user.Permissions = []string{dataprovider.PermCreateDirs, dataprovider.PermDelete, dataprovider.PermDownload}
|
user.Permissions = []string{dataprovider.PermCreateDirs, dataprovider.PermDelete, dataprovider.PermDownload}
|
||||||
user.UploadBandwidth = 1024
|
user.UploadBandwidth = 1024
|
||||||
user.DownloadBandwidth = 512
|
user.DownloadBandwidth = 512
|
||||||
user, _, err = api.UpdateUser(user, http.StatusOK)
|
user, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to update user: %v", err)
|
t.Errorf("unable to update user: %v", err)
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove: %v", err)
|
t.Errorf("unable to remove: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateUserNoCredentials(t *testing.T) {
|
func TestUpdateUserNoCredentials(t *testing.T) {
|
||||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -241,184 +243,184 @@ func TestUpdateUserNoCredentials(t *testing.T) {
|
||||||
user.PublicKeys = []string{}
|
user.PublicKeys = []string{}
|
||||||
// password and public key will be omitted from json serialization if empty and so they will remain unchanged
|
// password and public key will be omitted from json serialization if empty and so they will remain unchanged
|
||||||
// and no validation error will be raised
|
// and no validation error will be raised
|
||||||
_, _, err = api.UpdateUser(user, http.StatusOK)
|
_, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error updating user with no credentials: %v", err)
|
t.Errorf("unexpected error updating user with no credentials: %v", err)
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove: %v", err)
|
t.Errorf("unable to remove: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateUserEmptyHomeDir(t *testing.T) {
|
func TestUpdateUserEmptyHomeDir(t *testing.T) {
|
||||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
user.HomeDir = ""
|
user.HomeDir = ""
|
||||||
_, _, err = api.UpdateUser(user, http.StatusBadRequest)
|
_, _, err = httpd.UpdateUser(user, http.StatusBadRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error updating user with empty home dir: %v", err)
|
t.Errorf("unexpected error updating user with empty home dir: %v", err)
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove: %v", err)
|
t.Errorf("unable to remove: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateUserInvalidHomeDir(t *testing.T) {
|
func TestUpdateUserInvalidHomeDir(t *testing.T) {
|
||||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
user.HomeDir = "relative_path"
|
user.HomeDir = "relative_path"
|
||||||
_, _, err = api.UpdateUser(user, http.StatusBadRequest)
|
_, _, err = httpd.UpdateUser(user, http.StatusBadRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error updating user with empty home dir: %v", err)
|
t.Errorf("unexpected error updating user with empty home dir: %v", err)
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove: %v", err)
|
t.Errorf("unable to remove: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateNonExistentUser(t *testing.T) {
|
func TestUpdateNonExistentUser(t *testing.T) {
|
||||||
_, _, err := api.UpdateUser(getTestUser(), http.StatusNotFound)
|
_, _, err := httpd.UpdateUser(getTestUser(), http.StatusNotFound)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to update user: %v", err)
|
t.Errorf("unable to update user: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetNonExistentUser(t *testing.T) {
|
func TestGetNonExistentUser(t *testing.T) {
|
||||||
_, _, err := api.GetUserByID(0, http.StatusNotFound)
|
_, _, err := httpd.GetUserByID(0, http.StatusNotFound)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to get user: %v", err)
|
t.Errorf("unable to get user: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteNonExistentUser(t *testing.T) {
|
func TestDeleteNonExistentUser(t *testing.T) {
|
||||||
_, err := api.RemoveUser(getTestUser(), http.StatusNotFound)
|
_, err := httpd.RemoveUser(getTestUser(), http.StatusNotFound)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddDuplicateUser(t *testing.T) {
|
func TestAddDuplicateUser(t *testing.T) {
|
||||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
_, _, err = api.AddUser(getTestUser(), http.StatusInternalServerError)
|
_, _, err = httpd.AddUser(getTestUser(), http.StatusInternalServerError)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add second user: %v", err)
|
t.Errorf("unable to add second user: %v", err)
|
||||||
}
|
}
|
||||||
_, _, err = api.AddUser(getTestUser(), http.StatusOK)
|
_, _, err = httpd.AddUser(getTestUser(), http.StatusOK)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("adding a duplicate user must fail")
|
t.Errorf("adding a duplicate user must fail")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetUsers(t *testing.T) {
|
func TestGetUsers(t *testing.T) {
|
||||||
user1, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
user1, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
u.Username = defaultUsername + "1"
|
u.Username = defaultUsername + "1"
|
||||||
user2, _, err := api.AddUser(u, http.StatusOK)
|
user2, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add second user: %v", err)
|
t.Errorf("unable to add second user: %v", err)
|
||||||
}
|
}
|
||||||
users, _, err := api.GetUsers(0, 0, "", http.StatusOK)
|
users, _, err := httpd.GetUsers(0, 0, "", http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to get users: %v", err)
|
t.Errorf("unable to get users: %v", err)
|
||||||
}
|
}
|
||||||
if len(users) < 2 {
|
if len(users) < 2 {
|
||||||
t.Errorf("at least 2 users are expected")
|
t.Errorf("at least 2 users are expected")
|
||||||
}
|
}
|
||||||
users, _, err = api.GetUsers(1, 0, "", http.StatusOK)
|
users, _, err = httpd.GetUsers(1, 0, "", http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to get users: %v", err)
|
t.Errorf("unable to get users: %v", err)
|
||||||
}
|
}
|
||||||
if len(users) != 1 {
|
if len(users) != 1 {
|
||||||
t.Errorf("1 user is expected")
|
t.Errorf("1 user is expected")
|
||||||
}
|
}
|
||||||
users, _, err = api.GetUsers(1, 1, "", http.StatusOK)
|
users, _, err = httpd.GetUsers(1, 1, "", http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to get users: %v", err)
|
t.Errorf("unable to get users: %v", err)
|
||||||
}
|
}
|
||||||
if len(users) != 1 {
|
if len(users) != 1 {
|
||||||
t.Errorf("1 user is expected")
|
t.Errorf("1 user is expected")
|
||||||
}
|
}
|
||||||
_, _, err = api.GetUsers(1, 1, "", http.StatusInternalServerError)
|
_, _, err = httpd.GetUsers(1, 1, "", http.StatusInternalServerError)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("get users must succeed, we requested a fail for a good request")
|
t.Errorf("get users must succeed, we requested a fail for a good request")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user1, http.StatusOK)
|
_, err = httpd.RemoveUser(user1, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user2, http.StatusOK)
|
_, err = httpd.RemoveUser(user2, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetQuotaScans(t *testing.T) {
|
func TestGetQuotaScans(t *testing.T) {
|
||||||
_, _, err := api.GetQuotaScans(http.StatusOK)
|
_, _, err := httpd.GetQuotaScans(http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to get quota scans: %v", err)
|
t.Errorf("unable to get quota scans: %v", err)
|
||||||
}
|
}
|
||||||
_, _, err = api.GetQuotaScans(http.StatusInternalServerError)
|
_, _, err = httpd.GetQuotaScans(http.StatusInternalServerError)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("quota scan request must succeed, we requested to check a wrong status code")
|
t.Errorf("quota scan request must succeed, we requested to check a wrong status code")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStartQuotaScan(t *testing.T) {
|
func TestStartQuotaScan(t *testing.T) {
|
||||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
_, err = api.StartQuotaScan(user, http.StatusCreated)
|
_, err = httpd.StartQuotaScan(user, http.StatusCreated)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to start quota scan: %v", err)
|
t.Errorf("unable to start quota scan: %v", err)
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetVersion(t *testing.T) {
|
func TestGetVersion(t *testing.T) {
|
||||||
_, _, err := api.GetVersion(http.StatusOK)
|
_, _, err := httpd.GetVersion(http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to get sftp version: %v", err)
|
t.Errorf("unable to get sftp version: %v", err)
|
||||||
}
|
}
|
||||||
_, _, err = api.GetVersion(http.StatusInternalServerError)
|
_, _, err = httpd.GetVersion(http.StatusInternalServerError)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("get version request must succeed, we requested to check a wrong status code")
|
t.Errorf("get version request must succeed, we requested to check a wrong status code")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetConnections(t *testing.T) {
|
func TestGetConnections(t *testing.T) {
|
||||||
_, _, err := api.GetConnections(http.StatusOK)
|
_, _, err := httpd.GetConnections(http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to get sftp connections: %v", err)
|
t.Errorf("unable to get sftp connections: %v", err)
|
||||||
}
|
}
|
||||||
_, _, err = api.GetConnections(http.StatusInternalServerError)
|
_, _, err = httpd.GetConnections(http.StatusInternalServerError)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("get sftp connections request must succeed, we requested to check a wrong status code")
|
t.Errorf("get sftp connections request must succeed, we requested to check a wrong status code")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloseActiveConnection(t *testing.T) {
|
func TestCloseActiveConnection(t *testing.T) {
|
||||||
_, err := api.CloseConnection("non_existent_id", http.StatusNotFound)
|
_, err := httpd.CloseConnection("non_existent_id", http.StatusNotFound)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error closing non existent sftp connection: %v", err)
|
t.Errorf("unexpected error closing non existent sftp connection: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -434,17 +436,17 @@ func TestUserBaseDir(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error initializing data provider with users base dir")
|
t.Errorf("error initializing data provider with users base dir")
|
||||||
}
|
}
|
||||||
api.SetDataProvider(dataprovider.GetProvider())
|
httpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
u.HomeDir = ""
|
u.HomeDir = ""
|
||||||
user, _, err := api.AddUser(getTestUser(), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
if user.HomeDir != filepath.Join(providerConf.UsersBaseDir, u.Username) {
|
if user.HomeDir != filepath.Join(providerConf.UsersBaseDir, u.Username) {
|
||||||
t.Errorf("invalid home dir: %v", user.HomeDir)
|
t.Errorf("invalid home dir: %v", user.HomeDir)
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove: %v", err)
|
t.Errorf("unable to remove: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -456,26 +458,29 @@ func TestUserBaseDir(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error initializing data provider")
|
t.Errorf("error initializing data provider")
|
||||||
}
|
}
|
||||||
api.SetDataProvider(dataprovider.GetProvider())
|
httpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
sftpd.SetDataProvider(dataprovider.GetProvider())
|
sftpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProviderErrors(t *testing.T) {
|
func TestProviderErrors(t *testing.T) {
|
||||||
|
if providerDriverName == dataprovider.BoltDataProviderName {
|
||||||
|
t.Skip("skipping test provider errors for bolt provider")
|
||||||
|
}
|
||||||
dataProvider := dataprovider.GetProvider()
|
dataProvider := dataprovider.GetProvider()
|
||||||
dataprovider.Close(dataProvider)
|
dataprovider.Close(dataProvider)
|
||||||
_, _, err := api.GetUserByID(0, http.StatusInternalServerError)
|
_, _, err := httpd.GetUserByID(0, http.StatusInternalServerError)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("get user with provider closed must fail: %v", err)
|
t.Errorf("get user with provider closed must fail: %v", err)
|
||||||
}
|
}
|
||||||
_, _, err = api.GetUsers(0, 0, defaultUsername, http.StatusInternalServerError)
|
_, _, err = httpd.GetUsers(1, 0, defaultUsername, http.StatusInternalServerError)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("get users with provider closed must fail: %v", err)
|
t.Errorf("get users with provider closed must fail: %v", err)
|
||||||
}
|
}
|
||||||
_, _, err = api.UpdateUser(dataprovider.User{}, http.StatusInternalServerError)
|
_, _, err = httpd.UpdateUser(dataprovider.User{}, http.StatusInternalServerError)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("update user with provider closed must fail: %v", err)
|
t.Errorf("update user with provider closed must fail: %v", err)
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(dataprovider.User{}, http.StatusInternalServerError)
|
_, err = httpd.RemoveUser(dataprovider.User{}, http.StatusInternalServerError)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("delete user with provider closed must fail: %v", err)
|
t.Errorf("delete user with provider closed must fail: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -485,7 +490,7 @@ func TestProviderErrors(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error initializing data provider")
|
t.Errorf("error initializing data provider")
|
||||||
}
|
}
|
||||||
api.SetDataProvider(dataprovider.GetProvider())
|
httpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
sftpd.SetDataProvider(dataprovider.GetProvider())
|
sftpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -506,6 +511,7 @@ func TestBasicUserHandlingMock(t *testing.T) {
|
||||||
checkResponseCode(t, http.StatusInternalServerError, rr.Code)
|
checkResponseCode(t, http.StatusInternalServerError, rr.Code)
|
||||||
user.MaxSessions = 10
|
user.MaxSessions = 10
|
||||||
user.UploadBandwidth = 128
|
user.UploadBandwidth = 128
|
||||||
|
user.Permissions = []string{dataprovider.PermAny, dataprovider.PermDelete, dataprovider.PermDownload}
|
||||||
userAsJSON = getUserAsJSON(t, user)
|
userAsJSON = getUserAsJSON(t, user)
|
||||||
req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON))
|
req, _ = http.NewRequest(http.MethodPut, userPath+"/"+strconv.FormatInt(user.ID, 10), bytes.NewBuffer(userAsJSON))
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
|
@ -523,6 +529,12 @@ func TestBasicUserHandlingMock(t *testing.T) {
|
||||||
if user.MaxSessions != updatedUser.MaxSessions || user.UploadBandwidth != updatedUser.UploadBandwidth {
|
if user.MaxSessions != updatedUser.MaxSessions || user.UploadBandwidth != updatedUser.UploadBandwidth {
|
||||||
t.Errorf("Error modifying user actual: %v, %v", updatedUser.MaxSessions, updatedUser.UploadBandwidth)
|
t.Errorf("Error modifying user actual: %v, %v", updatedUser.MaxSessions, updatedUser.UploadBandwidth)
|
||||||
}
|
}
|
||||||
|
if len(updatedUser.Permissions) != 1 {
|
||||||
|
t.Errorf("permissions other than any should be removed")
|
||||||
|
}
|
||||||
|
if !utils.IsStringInSlice(dataprovider.PermAny, updatedUser.Permissions) {
|
||||||
|
t.Errorf("permissions mismatch")
|
||||||
|
}
|
||||||
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
|
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
@ -782,6 +794,275 @@ func TestMetricsMock(t *testing.T) {
|
||||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetWebRootMock(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
rr := executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusMovedPermanently, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, webBasePath, nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusMovedPermanently, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasicWebUsersMock(t *testing.T) {
|
||||||
|
user := getTestUser()
|
||||||
|
userAsJSON := getUserAsJSON(t, user)
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||||
|
rr := executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
err := render.DecodeJSON(rr.Body, &user)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error get user: %v", err)
|
||||||
|
}
|
||||||
|
user1 := getTestUser()
|
||||||
|
user1.Username += "1"
|
||||||
|
user1AsJSON := getUserAsJSON(t, user1)
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(user1AsJSON))
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
err = render.DecodeJSON(rr.Body, &user1)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error get user1: %v", err)
|
||||||
|
}
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, webUsersPath, nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, webUsersPath+"?qlimit=a", nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, webUsersPath+"?qlimit=1", nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, webUserPath, nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, webUserPath+"/"+strconv.FormatInt(user.ID, 10), nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, webUserPath+"/0", nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusNotFound, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, webUserPath+"/a", nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||||
|
form := make(url.Values)
|
||||||
|
form.Set("username", user.Username)
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode()))
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), strings.NewReader(form.Encode()))
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath+"/0", strings.NewReader(form.Encode()))
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusNotFound, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath+"/a", strings.NewReader(form.Encode()))
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusBadRequest, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user1.ID, 10), nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebUserAddMock(t *testing.T) {
|
||||||
|
user := getTestUser()
|
||||||
|
user.UploadBandwidth = 32
|
||||||
|
user.DownloadBandwidth = 64
|
||||||
|
user.UID = 1000
|
||||||
|
form := make(url.Values)
|
||||||
|
form.Set("username", user.Username)
|
||||||
|
form.Set("home_dir", user.HomeDir)
|
||||||
|
form.Set("password", user.Password)
|
||||||
|
form.Set("permissions", "*")
|
||||||
|
// test invalid url escape
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, webUserPath+"?a=%2", strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr := executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
form.Set("public_keys", testPubKey)
|
||||||
|
form.Set("uid", strconv.FormatInt(int64(user.UID), 10))
|
||||||
|
form.Set("gid", "a")
|
||||||
|
// test invalid gid
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
form.Set("gid", "0")
|
||||||
|
form.Set("max_sessions", "a")
|
||||||
|
// test invalid max sessions
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
form.Set("max_sessions", "0")
|
||||||
|
form.Set("quota_size", "a")
|
||||||
|
// test invalid quota size
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
form.Set("quota_size", "0")
|
||||||
|
form.Set("quota_files", "a")
|
||||||
|
// test invalid quota files
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
form.Set("quota_files", "0")
|
||||||
|
form.Set("upload_bandwidth", "a")
|
||||||
|
// test invalid upload bandwidth
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
form.Set("upload_bandwidth", strconv.FormatInt(user.UploadBandwidth, 10))
|
||||||
|
form.Set("download_bandwidth", "a")
|
||||||
|
// test invalid download bandwidth
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
form.Set("download_bandwidth", strconv.FormatInt(user.DownloadBandwidth, 10))
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusSeeOther, rr.Code)
|
||||||
|
// the user already exists, was created with the above request
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath, strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
var users []dataprovider.User
|
||||||
|
err := render.DecodeJSON(rr.Body, &users)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error decoding users: %v", err)
|
||||||
|
}
|
||||||
|
if len(users) != 1 {
|
||||||
|
t.Errorf("1 user is expected")
|
||||||
|
}
|
||||||
|
newUser := users[0]
|
||||||
|
if newUser.UID != user.UID {
|
||||||
|
t.Errorf("uid does not match")
|
||||||
|
}
|
||||||
|
if newUser.UploadBandwidth != user.UploadBandwidth {
|
||||||
|
t.Errorf("upload_bandwidth does not match")
|
||||||
|
}
|
||||||
|
if newUser.DownloadBandwidth != user.DownloadBandwidth {
|
||||||
|
t.Errorf("download_bandwidth does not match")
|
||||||
|
}
|
||||||
|
if !utils.IsStringInSlice(testPubKey, newUser.PublicKeys) {
|
||||||
|
t.Errorf("public_keys does not match")
|
||||||
|
}
|
||||||
|
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(newUser.ID, 10), nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebUserUpdateMock(t *testing.T) {
|
||||||
|
user := getTestUser()
|
||||||
|
userAsJSON := getUserAsJSON(t, user)
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||||
|
rr := executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
err := render.DecodeJSON(rr.Body, &user)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error get user: %v", err)
|
||||||
|
}
|
||||||
|
user.MaxSessions = 1
|
||||||
|
user.QuotaFiles = 2
|
||||||
|
user.QuotaSize = 3
|
||||||
|
user.GID = 1000
|
||||||
|
form := make(url.Values)
|
||||||
|
form.Set("username", user.Username)
|
||||||
|
form.Set("home_dir", user.HomeDir)
|
||||||
|
form.Set("uid", "0")
|
||||||
|
form.Set("gid", strconv.FormatInt(int64(user.GID), 10))
|
||||||
|
form.Set("max_sessions", strconv.FormatInt(int64(user.MaxSessions), 10))
|
||||||
|
form.Set("quota_size", strconv.FormatInt(user.QuotaSize, 10))
|
||||||
|
form.Set("quota_files", strconv.FormatInt(int64(user.QuotaFiles), 10))
|
||||||
|
form.Set("upload_bandwidth", "0")
|
||||||
|
form.Set("download_bandwidth", "0")
|
||||||
|
form.Set("permissions", "*")
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath+"/"+strconv.FormatInt(user.ID, 10), strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusSeeOther, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, userPath+"?limit=1&offset=0&order=ASC&username="+user.Username, nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
var users []dataprovider.User
|
||||||
|
err = render.DecodeJSON(rr.Body, &users)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error decoding users: %v", err)
|
||||||
|
}
|
||||||
|
if len(users) != 1 {
|
||||||
|
t.Errorf("1 user is expected")
|
||||||
|
}
|
||||||
|
updateUser := users[0]
|
||||||
|
if user.HomeDir != updateUser.HomeDir {
|
||||||
|
t.Errorf("home dir does not match")
|
||||||
|
}
|
||||||
|
if user.MaxSessions != updateUser.MaxSessions {
|
||||||
|
t.Errorf("max_sessions does not match")
|
||||||
|
}
|
||||||
|
if user.QuotaFiles != updateUser.QuotaFiles {
|
||||||
|
t.Errorf("quota_files does not match")
|
||||||
|
}
|
||||||
|
if user.QuotaSize != updateUser.QuotaSize {
|
||||||
|
t.Errorf("quota_size does not match")
|
||||||
|
}
|
||||||
|
if user.GID != updateUser.GID {
|
||||||
|
t.Errorf("gid does not match")
|
||||||
|
}
|
||||||
|
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderClosedMock(t *testing.T) {
|
||||||
|
if providerDriverName == dataprovider.BoltDataProviderName {
|
||||||
|
t.Skip("skipping test provider errors for bolt provider")
|
||||||
|
}
|
||||||
|
dataProvider := dataprovider.GetProvider()
|
||||||
|
dataprovider.Close(dataProvider)
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, webUsersPath, nil)
|
||||||
|
rr := executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusInternalServerError, rr.Code)
|
||||||
|
req, _ = http.NewRequest(http.MethodGet, webUserPath+"/0", nil)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusInternalServerError, rr.Code)
|
||||||
|
form := make(url.Values)
|
||||||
|
form.Set("username", "test")
|
||||||
|
req, _ = http.NewRequest(http.MethodPost, webUserPath+"/0", strings.NewReader(form.Encode()))
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusInternalServerError, rr.Code)
|
||||||
|
config.LoadConfig(configDir, "")
|
||||||
|
providerConf := config.GetProviderConf()
|
||||||
|
err := dataprovider.Initialize(providerConf, configDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error initializing data provider")
|
||||||
|
}
|
||||||
|
httpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
|
sftpd.SetDataProvider(dataprovider.GetProvider())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetWebConnectionsMock(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, webConnectionsPath, nil)
|
||||||
|
rr := executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStaticFilesMock(t *testing.T) {
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, "/static/favicon.ico", nil)
|
||||||
|
rr := executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func waitTCPListening(address string) {
|
func waitTCPListening(address string) {
|
||||||
for {
|
for {
|
||||||
conn, err := net.Dial("tcp", address)
|
conn, err := net.Dial("tcp", address)
|
|
@ -1,4 +1,4 @@
|
||||||
package api
|
package httpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/dataprovider"
|
"github.com/drakkan/sftpgo/dataprovider"
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
|
@ -226,3 +227,17 @@ func TestCloseConnectionHandler(t *testing.T) {
|
||||||
t.Errorf("Expected response code 400. Got %d", rr.Code)
|
t.Errorf("Expected response code 400. Got %d", rr.Code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderInvalidTemplate(t *testing.T) {
|
||||||
|
tmpl, err := template.New("test").Parse("{{.Count}}")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error making test template: %v", err)
|
||||||
|
} else {
|
||||||
|
templates["no_match"] = tmpl
|
||||||
|
rw := httptest.NewRecorder()
|
||||||
|
renderTemplate(rw, "no_match", map[string]string{})
|
||||||
|
if rw.Code != http.StatusInternalServerError {
|
||||||
|
t.Errorf("invalid template rendering must fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package api
|
package httpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -17,7 +17,7 @@ func GetHTTPRouter() http.Handler {
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
func initializeRouter() {
|
func initializeRouter(staticFilesPath string) {
|
||||||
router = chi.NewRouter()
|
router = chi.NewRouter()
|
||||||
router.Use(middleware.RequestID)
|
router.Use(middleware.RequestID)
|
||||||
router.Use(middleware.RealIP)
|
router.Use(middleware.RealIP)
|
||||||
|
@ -32,6 +32,14 @@ func initializeRouter() {
|
||||||
sendAPIResponse(w, r, nil, "Method not allowed", http.StatusMethodNotAllowed)
|
sendAPIResponse(w, r, nil, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, webUsersPath, http.StatusMovedPermanently)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Get(webBasePath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, webUsersPath, http.StatusMovedPermanently)
|
||||||
|
})
|
||||||
|
|
||||||
router.Handle(metricsPath, promhttp.Handler())
|
router.Handle(metricsPath, promhttp.Handler())
|
||||||
|
|
||||||
router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {
|
router.Get(versionPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -73,6 +81,35 @@ func initializeRouter() {
|
||||||
router.Delete(userPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
|
router.Delete(userPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
deleteUser(w, r)
|
deleteUser(w, r)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.Get(webUsersPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handleGetWebUsers(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Get(webUserPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handleWebAddUserGet(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Get(webUserPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handleWebUpdateUserGet(chi.URLParam(r, "userID"), w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Post(webUserPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handleWebAddUserPost(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Post(webUserPath+"/{userID}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handleWebUpdateUserPost(chi.URLParam(r, "userID"), w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Get(webConnectionsPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handleWebGetConnections(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Group(func(router chi.Router) {
|
||||||
|
router.Use(middleware.DefaultCompress)
|
||||||
|
fileServer(router, staticFileWebPath, http.Dir(staticFilesPath))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCloseConnection(w http.ResponseWriter, r *http.Request) {
|
func handleCloseConnection(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -87,3 +124,17 @@ func handleCloseConnection(w http.ResponseWriter, r *http.Request) {
|
||||||
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
|
sendAPIResponse(w, r, nil, "Not Found", http.StatusNotFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fileServer(r chi.Router, path string, root http.FileSystem) {
|
||||||
|
fs := http.StripPrefix(path, http.FileServer(root))
|
||||||
|
|
||||||
|
if path != "/" && path[len(path)-1] != '/' {
|
||||||
|
r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
|
||||||
|
path += "/"
|
||||||
|
}
|
||||||
|
path += "*"
|
||||||
|
|
||||||
|
r.Get(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fs.ServeHTTP(w, r)
|
||||||
|
}))
|
||||||
|
}
|
|
@ -184,7 +184,7 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- users
|
- users
|
||||||
summary: Returns an array with one or more users
|
summary: Returns an array with one or more users
|
||||||
description: For security reasons passwords are empty in the response
|
description: For security reasons hashed passwords are omitted in the response
|
||||||
operationId: get_users
|
operationId: get_users
|
||||||
parameters:
|
parameters:
|
||||||
- in: query
|
- in: query
|
||||||
|
@ -311,7 +311,7 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- users
|
- users
|
||||||
summary: Find user by ID
|
summary: Find user by ID
|
||||||
description: For security reasons passwords are empty in the response
|
description: For security reasons the hashed password is omitted in the response
|
||||||
operationId: get_user_by_id
|
operationId: get_user_by_id
|
||||||
parameters:
|
parameters:
|
||||||
- name: userID
|
- name: userID
|
||||||
|
@ -568,7 +568,7 @@ components:
|
||||||
quota_size:
|
quota_size:
|
||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
description: quota as size. 0 menas unlimited. Please note that quota is updated if files are added/removed via SFTP/SCP otherwise a quota scan is needed
|
description: quota as size in bytes. 0 menas unlimited. Please note that quota is updated if files are added/removed via SFTP/SCP otherwise a quota scan is needed
|
||||||
quota_files:
|
quota_files:
|
||||||
type: integer
|
type: integer
|
||||||
format: int32
|
format: int32
|
340
httpd/web.go
Normal file
340
httpd/web.go
Normal file
|
@ -0,0 +1,340 @@
|
||||||
|
package httpd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/drakkan/sftpgo/dataprovider"
|
||||||
|
"github.com/drakkan/sftpgo/sftpd"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
templateBase = "base.html"
|
||||||
|
templateUsers = "users.html"
|
||||||
|
templateUser = "user.html"
|
||||||
|
templateConnections = "connections.html"
|
||||||
|
templateMessage = "message.html"
|
||||||
|
|
||||||
|
webBasePath = "/web"
|
||||||
|
webUsersPath = "/web/users"
|
||||||
|
webUserPath = "/web/user"
|
||||||
|
webConnectionsPath = "/web/connections"
|
||||||
|
staticFileWebPath = "/static"
|
||||||
|
|
||||||
|
pageUsersTitle = "Users"
|
||||||
|
pageConnectionsTitle = "Connections"
|
||||||
|
page400Title = "Bad request"
|
||||||
|
page404Title = "Not found"
|
||||||
|
page404Body = "The page you are looking for does not exist."
|
||||||
|
page500Title = "Internal Server Error"
|
||||||
|
page500Body = "The server is unable to fulfill your request."
|
||||||
|
defaultUsersQueryLimit = 500
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
templates = make(map[string]*template.Template)
|
||||||
|
)
|
||||||
|
|
||||||
|
type basePage struct {
|
||||||
|
Title string
|
||||||
|
CurrentURL string
|
||||||
|
UsersURL string
|
||||||
|
UserURL string
|
||||||
|
APIUserURL string
|
||||||
|
APIConnectionsURL string
|
||||||
|
ConnectionsURL string
|
||||||
|
UsersTitle string
|
||||||
|
ConnectionsTitle string
|
||||||
|
}
|
||||||
|
|
||||||
|
type usersPage struct {
|
||||||
|
basePage
|
||||||
|
Users []dataprovider.User
|
||||||
|
}
|
||||||
|
|
||||||
|
type connectionsPage struct {
|
||||||
|
basePage
|
||||||
|
Connections []sftpd.ConnectionStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
type userPage struct {
|
||||||
|
basePage
|
||||||
|
IsAdd bool
|
||||||
|
User dataprovider.User
|
||||||
|
Error string
|
||||||
|
ValidPerms []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type messagePage struct {
|
||||||
|
basePage
|
||||||
|
Error string
|
||||||
|
Success string
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTemplates(templatesPath string) {
|
||||||
|
usersPaths := []string{
|
||||||
|
filepath.Join(templatesPath, templateBase),
|
||||||
|
filepath.Join(templatesPath, templateUsers),
|
||||||
|
}
|
||||||
|
userPaths := []string{
|
||||||
|
filepath.Join(templatesPath, templateBase),
|
||||||
|
filepath.Join(templatesPath, templateUser),
|
||||||
|
}
|
||||||
|
connectionsPaths := []string{
|
||||||
|
filepath.Join(templatesPath, templateBase),
|
||||||
|
filepath.Join(templatesPath, templateConnections),
|
||||||
|
}
|
||||||
|
messagePath := []string{
|
||||||
|
filepath.Join(templatesPath, templateBase),
|
||||||
|
filepath.Join(templatesPath, templateMessage),
|
||||||
|
}
|
||||||
|
usersTmpl := template.Must(template.ParseFiles(usersPaths...))
|
||||||
|
userTmpl := template.Must(template.ParseFiles(userPaths...))
|
||||||
|
connectionsTmpl := template.Must(template.ParseFiles(connectionsPaths...))
|
||||||
|
messageTmpl := template.Must(template.ParseFiles(messagePath...))
|
||||||
|
|
||||||
|
templates[templateUsers] = usersTmpl
|
||||||
|
templates[templateUser] = userTmpl
|
||||||
|
templates[templateConnections] = connectionsTmpl
|
||||||
|
templates[templateMessage] = messageTmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBasePageData(title, currentURL string) basePage {
|
||||||
|
return basePage{
|
||||||
|
Title: title,
|
||||||
|
CurrentURL: currentURL,
|
||||||
|
UsersURL: webUsersPath,
|
||||||
|
UserURL: webUserPath,
|
||||||
|
APIUserURL: userPath,
|
||||||
|
APIConnectionsURL: activeConnectionsPath,
|
||||||
|
ConnectionsURL: webConnectionsPath,
|
||||||
|
UsersTitle: pageUsersTitle,
|
||||||
|
ConnectionsTitle: pageConnectionsTitle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTemplate(w http.ResponseWriter, tmplName string, data interface{}) {
|
||||||
|
err := templates[tmplName].ExecuteTemplate(w, tmplName, data)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderMessagePage(w http.ResponseWriter, title, body string, statusCode int, err error, message string) {
|
||||||
|
var errorString string
|
||||||
|
if len(body) > 0 {
|
||||||
|
errorString = body + " "
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
errorString += err.Error()
|
||||||
|
}
|
||||||
|
data := messagePage{
|
||||||
|
basePage: getBasePageData(title, ""),
|
||||||
|
Error: errorString,
|
||||||
|
Success: message,
|
||||||
|
}
|
||||||
|
w.WriteHeader(statusCode)
|
||||||
|
renderTemplate(w, templateMessage, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderInternalServerErrorPage(w http.ResponseWriter, err error) {
|
||||||
|
renderMessagePage(w, page500Title, page400Title, http.StatusInternalServerError, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderBadRequestPage(w http.ResponseWriter, err error) {
|
||||||
|
renderMessagePage(w, page400Title, "", http.StatusBadRequest, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderNotFoundPage(w http.ResponseWriter, err error) {
|
||||||
|
renderMessagePage(w, page404Title, page404Body, http.StatusNotFound, err, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
|
||||||
|
data := userPage{
|
||||||
|
basePage: getBasePageData("Add a new user", webUserPath),
|
||||||
|
IsAdd: true,
|
||||||
|
Error: error,
|
||||||
|
User: user,
|
||||||
|
ValidPerms: dataprovider.ValidPerms,
|
||||||
|
}
|
||||||
|
renderTemplate(w, templateUser, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
|
||||||
|
data := userPage{
|
||||||
|
basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, user.ID)),
|
||||||
|
IsAdd: false,
|
||||||
|
Error: error,
|
||||||
|
User: user,
|
||||||
|
ValidPerms: dataprovider.ValidPerms,
|
||||||
|
}
|
||||||
|
renderTemplate(w, templateUser, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
||||||
|
var user dataprovider.User
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
publicKeysFormValue := r.Form.Get("public_keys")
|
||||||
|
publicKeys := []string{}
|
||||||
|
for _, v := range strings.Split(publicKeysFormValue, "\n") {
|
||||||
|
cleaned := strings.TrimSpace(v)
|
||||||
|
if len(cleaned) > 0 {
|
||||||
|
publicKeys = append(publicKeys, cleaned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uid, err := strconv.Atoi(r.Form.Get("uid"))
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
gid, err := strconv.Atoi(r.Form.Get("gid"))
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
maxSessions, err := strconv.Atoi(r.Form.Get("max_sessions"))
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
quotaSize, err := strconv.ParseInt(r.Form.Get("quota_size"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
quotaFiles, err := strconv.Atoi(r.Form.Get("quota_files"))
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
bandwidthUL, err := strconv.ParseInt(r.Form.Get("upload_bandwidth"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
bandwidthDL, err := strconv.ParseInt(r.Form.Get("download_bandwidth"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
user = dataprovider.User{
|
||||||
|
Username: r.Form.Get("username"),
|
||||||
|
Password: r.Form.Get("password"),
|
||||||
|
PublicKeys: publicKeys,
|
||||||
|
HomeDir: r.Form.Get("home_dir"),
|
||||||
|
UID: uid,
|
||||||
|
GID: gid,
|
||||||
|
Permissions: r.Form["permissions"],
|
||||||
|
MaxSessions: maxSessions,
|
||||||
|
QuotaSize: quotaSize,
|
||||||
|
QuotaFiles: quotaFiles,
|
||||||
|
UploadBandwidth: bandwidthUL,
|
||||||
|
DownloadBandwidth: bandwidthDL,
|
||||||
|
}
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
limit := defaultUsersQueryLimit
|
||||||
|
if _, ok := r.URL.Query()["qlimit"]; ok {
|
||||||
|
var err error
|
||||||
|
limit, err = strconv.Atoi(r.URL.Query().Get("qlimit"))
|
||||||
|
if err != nil {
|
||||||
|
limit = defaultUsersQueryLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var users []dataprovider.User
|
||||||
|
u, err := dataprovider.GetUsers(dataProvider, limit, 0, "ASC", "")
|
||||||
|
users = append(users, u...)
|
||||||
|
for len(u) == limit {
|
||||||
|
u, err = dataprovider.GetUsers(dataProvider, limit, len(users), "ASC", "")
|
||||||
|
if err == nil && len(u) > 0 {
|
||||||
|
users = append(users, u...)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
renderInternalServerErrorPage(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := usersPage{
|
||||||
|
basePage: getBasePageData(pageUsersTitle, webUsersPath),
|
||||||
|
Users: users,
|
||||||
|
}
|
||||||
|
renderTemplate(w, templateUsers, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
renderAddUserPage(w, dataprovider.User{}, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWebUpdateUserGet(userID string, w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.ParseInt(userID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
renderBadRequestPage(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, err := dataprovider.GetUserByID(dataProvider, id)
|
||||||
|
if err == nil {
|
||||||
|
renderUpdateUserPage(w, user, "")
|
||||||
|
} else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||||
|
renderNotFoundPage(w, err)
|
||||||
|
} else {
|
||||||
|
renderInternalServerErrorPage(w, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user, err := getUserFromPostFields(r)
|
||||||
|
if err != nil {
|
||||||
|
renderAddUserPage(w, user, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = dataprovider.AddUser(dataProvider, user)
|
||||||
|
if err == nil {
|
||||||
|
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
|
||||||
|
} else {
|
||||||
|
renderAddUserPage(w, user, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWebUpdateUserPost(userID string, w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, err := strconv.ParseInt(userID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
renderBadRequestPage(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, err := dataprovider.GetUserByID(dataProvider, id)
|
||||||
|
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
|
||||||
|
renderNotFoundPage(w, err)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
renderInternalServerErrorPage(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updatedUser, err := getUserFromPostFields(r)
|
||||||
|
if err != nil {
|
||||||
|
renderUpdateUserPage(w, user, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updatedUser.ID = user.ID
|
||||||
|
if len(updatedUser.Password) == 0 {
|
||||||
|
updatedUser.Password = user.Password
|
||||||
|
}
|
||||||
|
err = dataprovider.UpdateUser(dataProvider, updatedUser)
|
||||||
|
if err == nil {
|
||||||
|
http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
|
||||||
|
} else {
|
||||||
|
renderUpdateUserPage(w, user, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWebGetConnections(w http.ResponseWriter, r *http.Request) {
|
||||||
|
connectionStats := sftpd.GetConnectionsStats()
|
||||||
|
data := connectionsPage{
|
||||||
|
basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath),
|
||||||
|
Connections: connectionStats,
|
||||||
|
}
|
||||||
|
renderTemplate(w, templateConnections, data)
|
||||||
|
}
|
|
@ -1,13 +1,9 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/api"
|
|
||||||
"github.com/drakkan/sftpgo/config"
|
"github.com/drakkan/sftpgo/config"
|
||||||
"github.com/drakkan/sftpgo/dataprovider"
|
"github.com/drakkan/sftpgo/dataprovider"
|
||||||
|
"github.com/drakkan/sftpgo/httpd"
|
||||||
"github.com/drakkan/sftpgo/logger"
|
"github.com/drakkan/sftpgo/logger"
|
||||||
"github.com/drakkan/sftpgo/sftpd"
|
"github.com/drakkan/sftpgo/sftpd"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
@ -66,19 +62,10 @@ func (s *Service) Start() error {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if httpdConf.BindPort > 0 {
|
if httpdConf.BindPort > 0 {
|
||||||
router := api.GetHTTPRouter()
|
httpd.SetDataProvider(dataProvider)
|
||||||
api.SetDataProvider(dataProvider)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
logger.Debug(logSender, "", "initializing HTTP server with config %+v", httpdConf)
|
if err := httpdConf.Initialize(s.ConfigDir); err != nil {
|
||||||
httpServer := &http.Server{
|
|
||||||
Addr: fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort),
|
|
||||||
Handler: router,
|
|
||||||
ReadTimeout: 300 * time.Second,
|
|
||||||
WriteTimeout: 300 * time.Second,
|
|
||||||
MaxHeaderBytes: 1 << 20, // 1MB
|
|
||||||
}
|
|
||||||
if err := httpServer.ListenAndServe(); err != nil {
|
|
||||||
logger.Error(logSender, "", "could not start HTTP server: %v", err)
|
logger.Error(logSender, "", "could not start HTTP server: %v", err)
|
||||||
logger.ErrorToConsole("could not start HTTP server: %v", err)
|
logger.ErrorToConsole("could not start HTTP server: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/dataprovider"
|
"github.com/drakkan/sftpgo/dataprovider"
|
||||||
|
"github.com/drakkan/sftpgo/utils"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -722,3 +723,45 @@ func TestUploadError(t *testing.T) {
|
||||||
t.Errorf("file uploaded must be deleted after an error: %v", err)
|
t.Errorf("file uploaded must be deleted after an error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConnectionStatusStruct(t *testing.T) {
|
||||||
|
var transfers []connectionTransfer
|
||||||
|
transferUL := connectionTransfer{
|
||||||
|
OperationType: operationUpload,
|
||||||
|
StartTime: utils.GetTimeAsMsSinceEpoch(time.Now()),
|
||||||
|
Size: 123,
|
||||||
|
LastActivity: utils.GetTimeAsMsSinceEpoch(time.Now()),
|
||||||
|
Path: "/test.upload",
|
||||||
|
}
|
||||||
|
transferDL := connectionTransfer{
|
||||||
|
OperationType: operationDownload,
|
||||||
|
StartTime: utils.GetTimeAsMsSinceEpoch(time.Now()),
|
||||||
|
Size: 123,
|
||||||
|
LastActivity: utils.GetTimeAsMsSinceEpoch(time.Now()),
|
||||||
|
Path: "/test.download",
|
||||||
|
}
|
||||||
|
transfers = append(transfers, transferUL)
|
||||||
|
transfers = append(transfers, transferDL)
|
||||||
|
c := ConnectionStatus{
|
||||||
|
Username: "test",
|
||||||
|
ConnectionID: "123",
|
||||||
|
ClientVersion: "fakeClient-1.0.0",
|
||||||
|
RemoteAddress: "127.0.0.1:1234",
|
||||||
|
ConnectionTime: utils.GetTimeAsMsSinceEpoch(time.Now()),
|
||||||
|
LastActivity: utils.GetTimeAsMsSinceEpoch(time.Now()),
|
||||||
|
Protocol: "SFTP",
|
||||||
|
Transfers: transfers,
|
||||||
|
}
|
||||||
|
durationString := c.GetConnectionDuration()
|
||||||
|
if len(durationString) == 0 {
|
||||||
|
t.Errorf("error getting connection duration")
|
||||||
|
}
|
||||||
|
transfersString := c.GetTransfersAsString()
|
||||||
|
if len(transfersString) == 0 {
|
||||||
|
t.Errorf("error getting transfers as string")
|
||||||
|
}
|
||||||
|
connInfo := c.GetConnectionInfo()
|
||||||
|
if len(connInfo) == 0 {
|
||||||
|
t.Errorf("error getting connection info")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -101,6 +101,47 @@ func init() {
|
||||||
idleConnectionTicker = time.NewTicker(5 * time.Minute)
|
idleConnectionTicker = time.NewTicker(5 * time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetConnectionDuration returns the connection duration as string
|
||||||
|
func (c ConnectionStatus) GetConnectionDuration() string {
|
||||||
|
elapsed := time.Since(utils.GetTimeFromMsecSinceEpoch(c.ConnectionTime))
|
||||||
|
return utils.GetDurationAsString(elapsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConnectionInfo returns connection info.
|
||||||
|
// Protocol,Client Version and RemoteAddress are returned
|
||||||
|
func (c ConnectionStatus) GetConnectionInfo() string {
|
||||||
|
return fmt.Sprintf("%v. Client: %#v From: %#v", c.Protocol, c.ClientVersion, c.RemoteAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransfersAsString returns the active transfers as string
|
||||||
|
func (c ConnectionStatus) GetTransfersAsString() string {
|
||||||
|
result := ""
|
||||||
|
for _, t := range c.Transfers {
|
||||||
|
if len(result) > 0 {
|
||||||
|
result += ". "
|
||||||
|
}
|
||||||
|
result += t.getConnectionTransferAsString()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t connectionTransfer) getConnectionTransferAsString() string {
|
||||||
|
result := ""
|
||||||
|
if t.OperationType == operationUpload {
|
||||||
|
result += "UL"
|
||||||
|
} else {
|
||||||
|
result += "DL"
|
||||||
|
}
|
||||||
|
result += fmt.Sprintf(" %#v ", t.Path)
|
||||||
|
if t.Size > 0 {
|
||||||
|
elapsed := time.Since(utils.GetTimeFromMsecSinceEpoch(t.StartTime))
|
||||||
|
speed := float64(t.Size) / float64(utils.GetTimeAsMsSinceEpoch(time.Now())-t.StartTime)
|
||||||
|
result += fmt.Sprintf("Size: %#v Elapsed: %#v Speed: \"%.1f KB/s\"", utils.ByteCountSI(t.Size),
|
||||||
|
utils.GetDurationAsString(elapsed), speed)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// SetDataProvider sets the data provider to use to authenticate users and to get/update their disk quota
|
// SetDataProvider sets the data provider to use to authenticate users and to get/update their disk quota
|
||||||
func SetDataProvider(provider dataprovider.Provider) {
|
func SetDataProvider(provider dataprovider.Provider) {
|
||||||
dataProvider = provider
|
dataProvider = provider
|
||||||
|
|
|
@ -21,9 +21,9 @@ import (
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
"github.com/drakkan/sftpgo/api"
|
|
||||||
"github.com/drakkan/sftpgo/config"
|
"github.com/drakkan/sftpgo/config"
|
||||||
"github.com/drakkan/sftpgo/dataprovider"
|
"github.com/drakkan/sftpgo/dataprovider"
|
||||||
|
"github.com/drakkan/sftpgo/httpd"
|
||||||
"github.com/drakkan/sftpgo/logger"
|
"github.com/drakkan/sftpgo/logger"
|
||||||
"github.com/drakkan/sftpgo/sftpd"
|
"github.com/drakkan/sftpgo/sftpd"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
|
@ -103,7 +103,6 @@ func TestMain(m *testing.M) {
|
||||||
dataProvider := dataprovider.GetProvider()
|
dataProvider := dataprovider.GetProvider()
|
||||||
sftpdConf := config.GetSFTPDConfig()
|
sftpdConf := config.GetSFTPDConfig()
|
||||||
httpdConf := config.GetHTTPDConfig()
|
httpdConf := config.GetHTTPDConfig()
|
||||||
router := api.GetHTTPRouter()
|
|
||||||
sftpdConf.BindPort = 2022
|
sftpdConf.BindPort = 2022
|
||||||
sftpdConf.KexAlgorithms = []string{"curve25519-sha256@libssh.org", "ecdh-sha2-nistp256",
|
sftpdConf.KexAlgorithms = []string{"curve25519-sha256@libssh.org", "ecdh-sha2-nistp256",
|
||||||
"ecdh-sha2-nistp384"}
|
"ecdh-sha2-nistp384"}
|
||||||
|
@ -137,7 +136,7 @@ func TestMain(m *testing.M) {
|
||||||
}
|
}
|
||||||
|
|
||||||
sftpd.SetDataProvider(dataProvider)
|
sftpd.SetDataProvider(dataProvider)
|
||||||
api.SetDataProvider(dataProvider)
|
httpd.SetDataProvider(dataProvider)
|
||||||
|
|
||||||
scpPath, err = exec.LookPath("scp")
|
scpPath, err = exec.LookPath("scp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -154,15 +153,7 @@ func TestMain(m *testing.M) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
logger.Debug(logSender, "", "initializing HTTP server with config %+v", httpdConf)
|
if err := httpdConf.Initialize(configDir); err != nil {
|
||||||
s := &http.Server{
|
|
||||||
Addr: fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort),
|
|
||||||
Handler: router,
|
|
||||||
ReadTimeout: 300 * time.Second,
|
|
||||||
WriteTimeout: 300 * time.Second,
|
|
||||||
MaxHeaderBytes: 1 << 20, // 1MB
|
|
||||||
}
|
|
||||||
if err := s.ListenAndServe(); err != nil {
|
|
||||||
logger.Error(logSender, "", "could not start HTTP server: %v", err)
|
logger.Error(logSender, "", "could not start HTTP server: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -192,7 +183,7 @@ func TestBasicSFTPHandling(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.QuotaSize = 6553600
|
u.QuotaSize = 6553600
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -219,7 +210,7 @@ func TestBasicSFTPHandling(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("file download error: %v", err)
|
t.Errorf("file download error: %v", err)
|
||||||
}
|
}
|
||||||
user, _, err = api.GetUserByID(user.ID, http.StatusOK)
|
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error getting user: %v", err)
|
t.Errorf("error getting user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -237,7 +228,7 @@ func TestBasicSFTPHandling(t *testing.T) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("stat for deleted file must not succeed")
|
t.Errorf("stat for deleted file must not succeed")
|
||||||
}
|
}
|
||||||
user, _, err = api.GetUserByID(user.ID, http.StatusOK)
|
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error getting user: %v", err)
|
t.Errorf("error getting user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -248,7 +239,7 @@ func TestBasicSFTPHandling(t *testing.T) {
|
||||||
t.Errorf("quota size does not match, expected: %v, actual: %v", expectedQuotaSize-testFileSize, user.UsedQuotaSize)
|
t.Errorf("quota size does not match, expected: %v, actual: %v", expectedQuotaSize-testFileSize, user.UsedQuotaSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -257,7 +248,7 @@ func TestBasicSFTPHandling(t *testing.T) {
|
||||||
|
|
||||||
func TestDirCommands(t *testing.T) {
|
func TestDirCommands(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -318,7 +309,7 @@ func TestDirCommands(t *testing.T) {
|
||||||
t.Errorf("remove missing path must fail")
|
t.Errorf("remove missing path must fail")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -327,7 +318,7 @@ func TestDirCommands(t *testing.T) {
|
||||||
|
|
||||||
func TestLink(t *testing.T) {
|
func TestLink(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -372,7 +363,7 @@ func TestLink(t *testing.T) {
|
||||||
t.Errorf("error removing uploaded file: %v", err)
|
t.Errorf("error removing uploaded file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -381,7 +372,7 @@ func TestLink(t *testing.T) {
|
||||||
|
|
||||||
func TestStat(t *testing.T) {
|
func TestStat(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -429,7 +420,7 @@ func TestStat(t *testing.T) {
|
||||||
t.Errorf("error removing uploaded file: %v", err)
|
t.Errorf("error removing uploaded file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -439,7 +430,7 @@ func TestStat(t *testing.T) {
|
||||||
// basic tests to verify virtual chroot, should be improved to cover more cases ...
|
// basic tests to verify virtual chroot, should be improved to cover more cases ...
|
||||||
func TestEscapeHomeDir(t *testing.T) {
|
func TestEscapeHomeDir(t *testing.T) {
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -502,7 +493,7 @@ func TestEscapeHomeDir(t *testing.T) {
|
||||||
}
|
}
|
||||||
os.Remove(linkPath)
|
os.Remove(linkPath)
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -513,7 +504,7 @@ func TestHomeSpecialChars(t *testing.T) {
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.HomeDir = filepath.Join(homeBasePath, "abc açà#&%lk")
|
u.HomeDir = filepath.Join(homeBasePath, "abc açà#&%lk")
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -549,7 +540,7 @@ func TestHomeSpecialChars(t *testing.T) {
|
||||||
t.Errorf("error removing uploaded file: %v", err)
|
t.Errorf("error removing uploaded file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -559,7 +550,7 @@ func TestHomeSpecialChars(t *testing.T) {
|
||||||
func TestLogin(t *testing.T) {
|
func TestLogin(t *testing.T) {
|
||||||
u := getTestUser(false)
|
u := getTestUser(false)
|
||||||
u.PublicKeys = []string{testPubKey}
|
u.PublicKeys = []string{testPubKey}
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -592,7 +583,7 @@ func TestLogin(t *testing.T) {
|
||||||
// testPubKey1 is not authorized
|
// testPubKey1 is not authorized
|
||||||
user.PublicKeys = []string{testPubKey1}
|
user.PublicKeys = []string{testPubKey1}
|
||||||
user.Password = ""
|
user.Password = ""
|
||||||
_, _, err = api.UpdateUser(user, http.StatusOK)
|
_, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to update user: %v", err)
|
t.Errorf("unable to update user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -604,7 +595,7 @@ func TestLogin(t *testing.T) {
|
||||||
// login a user with multiple public keys, only the second one is valid
|
// login a user with multiple public keys, only the second one is valid
|
||||||
user.PublicKeys = []string{testPubKey1, testPubKey}
|
user.PublicKeys = []string{testPubKey1, testPubKey}
|
||||||
user.Password = ""
|
user.Password = ""
|
||||||
_, _, err = api.UpdateUser(user, http.StatusOK)
|
_, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to update user: %v", err)
|
t.Errorf("unable to update user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -618,7 +609,7 @@ func TestLogin(t *testing.T) {
|
||||||
t.Errorf("sftp client with multiple public key must work if at least one public key is valid")
|
t.Errorf("sftp client with multiple public key must work if at least one public key is valid")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -627,14 +618,14 @@ func TestLogin(t *testing.T) {
|
||||||
|
|
||||||
func TestLoginAfterUserUpdateEmptyPwd(t *testing.T) {
|
func TestLoginAfterUserUpdateEmptyPwd(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
user.Password = ""
|
user.Password = ""
|
||||||
user.PublicKeys = []string{}
|
user.PublicKeys = []string{}
|
||||||
// password and public key should remain unchanged
|
// password and public key should remain unchanged
|
||||||
_, _, err = api.UpdateUser(user, http.StatusOK)
|
_, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to update user: %v", err)
|
t.Errorf("unable to update user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -652,7 +643,7 @@ func TestLoginAfterUserUpdateEmptyPwd(t *testing.T) {
|
||||||
t.Errorf("unable to read remote dir: %v", err)
|
t.Errorf("unable to read remote dir: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -661,14 +652,14 @@ func TestLoginAfterUserUpdateEmptyPwd(t *testing.T) {
|
||||||
|
|
||||||
func TestLoginAfterUserUpdateEmptyPubKey(t *testing.T) {
|
func TestLoginAfterUserUpdateEmptyPubKey(t *testing.T) {
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
user.Password = ""
|
user.Password = ""
|
||||||
user.PublicKeys = []string{}
|
user.PublicKeys = []string{}
|
||||||
// password and public key should remain unchanged
|
// password and public key should remain unchanged
|
||||||
_, _, err = api.UpdateUser(user, http.StatusOK)
|
_, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to update user: %v", err)
|
t.Errorf("unable to update user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -686,7 +677,7 @@ func TestLoginAfterUserUpdateEmptyPubKey(t *testing.T) {
|
||||||
t.Errorf("unable to read remote dir: %v", err)
|
t.Errorf("unable to read remote dir: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -697,7 +688,7 @@ func TestMaxSessions(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.MaxSessions = 1
|
u.MaxSessions = 1
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -719,7 +710,7 @@ func TestMaxSessions(t *testing.T) {
|
||||||
t.Errorf("max sessions exceeded, new login should not succeed")
|
t.Errorf("max sessions exceeded, new login should not succeed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -730,10 +721,11 @@ func TestQuotaFileReplace(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.QuotaFiles = 1000
|
u.QuotaFiles = 1000
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
os.RemoveAll(user.GetHomeDir())
|
||||||
testFileSize := int64(65535)
|
testFileSize := int64(65535)
|
||||||
testFileName := "test_file.dat"
|
testFileName := "test_file.dat"
|
||||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||||
|
@ -752,7 +744,7 @@ func TestQuotaFileReplace(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("file upload error: %v", err)
|
t.Errorf("file upload error: %v", err)
|
||||||
}
|
}
|
||||||
user, _, err = api.GetUserByID(user.ID, http.StatusOK)
|
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error getting user: %v", err)
|
t.Errorf("error getting user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -761,7 +753,7 @@ func TestQuotaFileReplace(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("file upload error: %v", err)
|
t.Errorf("file upload error: %v", err)
|
||||||
}
|
}
|
||||||
user, _, err = api.GetUserByID(user.ID, http.StatusOK)
|
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error getting user: %v", err)
|
t.Errorf("error getting user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -774,7 +766,7 @@ func TestQuotaFileReplace(t *testing.T) {
|
||||||
}
|
}
|
||||||
// now set a quota size restriction and upload the same fail, upload should fail for space limit exceeded
|
// now set a quota size restriction and upload the same fail, upload should fail for space limit exceeded
|
||||||
user.QuotaSize = testFileSize - 1
|
user.QuotaSize = testFileSize - 1
|
||||||
user, _, err = api.UpdateUser(user, http.StatusOK)
|
user, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error updating user: %v", err)
|
t.Errorf("error updating user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -791,7 +783,7 @@ func TestQuotaFileReplace(t *testing.T) {
|
||||||
t.Errorf("error removing uploaded file: %v", err)
|
t.Errorf("error removing uploaded file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -800,7 +792,7 @@ func TestQuotaFileReplace(t *testing.T) {
|
||||||
|
|
||||||
func TestQuotaScan(t *testing.T) {
|
func TestQuotaScan(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -823,31 +815,31 @@ func TestQuotaScan(t *testing.T) {
|
||||||
t.Errorf("file upload error: %v", err)
|
t.Errorf("file upload error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
// create user with the same home dir, so there is at least an untracked file
|
// create user with the same home dir, so there is at least an untracked file
|
||||||
user, _, err = api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err = httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
_, err = api.StartQuotaScan(user, http.StatusCreated)
|
_, err = httpd.StartQuotaScan(user, http.StatusCreated)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error starting quota scan: %v", err)
|
t.Errorf("error starting quota scan: %v", err)
|
||||||
}
|
}
|
||||||
scans, _, err := api.GetQuotaScans(http.StatusOK)
|
scans, _, err := httpd.GetQuotaScans(http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error getting active quota scans: %v", err)
|
t.Errorf("error getting active quota scans: %v", err)
|
||||||
}
|
}
|
||||||
for len(scans) > 0 {
|
for len(scans) > 0 {
|
||||||
scans, _, err = api.GetQuotaScans(http.StatusOK)
|
scans, _, err = httpd.GetQuotaScans(http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error getting active quota scans: %v", err)
|
t.Errorf("error getting active quota scans: %v", err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
user, _, err = api.GetUserByID(user.ID, http.StatusOK)
|
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error getting user: %v", err)
|
t.Errorf("error getting user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -857,7 +849,7 @@ func TestQuotaScan(t *testing.T) {
|
||||||
if expectedQuotaSize != user.UsedQuotaSize {
|
if expectedQuotaSize != user.UsedQuotaSize {
|
||||||
t.Errorf("quota size does not match after scan, expected: %v, actual: %v", expectedQuotaSize, user.UsedQuotaSize)
|
t.Errorf("quota size does not match after scan, expected: %v, actual: %v", expectedQuotaSize, user.UsedQuotaSize)
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -880,7 +872,7 @@ func TestQuotaSize(t *testing.T) {
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.QuotaFiles = 1
|
u.QuotaFiles = 1
|
||||||
u.QuotaSize = testFileSize - 1
|
u.QuotaSize = testFileSize - 1
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -908,7 +900,7 @@ func TestQuotaSize(t *testing.T) {
|
||||||
t.Errorf("error removing uploaded file: %v", err)
|
t.Errorf("error removing uploaded file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -926,7 +918,7 @@ func TestBandwidthAndConnections(t *testing.T) {
|
||||||
// 100 ms tolerance
|
// 100 ms tolerance
|
||||||
wantedUploadElapsed -= 100
|
wantedUploadElapsed -= 100
|
||||||
wantedDownloadElapsed -= 100
|
wantedDownloadElapsed -= 100
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -980,7 +972,7 @@ func TestBandwidthAndConnections(t *testing.T) {
|
||||||
t.Errorf("connection closed upload must fail")
|
t.Errorf("connection closed upload must fail")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -990,7 +982,7 @@ func TestBandwidthAndConnections(t *testing.T) {
|
||||||
func TestMissingFile(t *testing.T) {
|
func TestMissingFile(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1005,7 +997,7 @@ func TestMissingFile(t *testing.T) {
|
||||||
t.Errorf("download missing file must fail")
|
t.Errorf("download missing file must fail")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1015,7 +1007,7 @@ func TestMissingFile(t *testing.T) {
|
||||||
func TestOverwriteDirWithFile(t *testing.T) {
|
func TestOverwriteDirWithFile(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1057,7 +1049,7 @@ func TestOverwriteDirWithFile(t *testing.T) {
|
||||||
t.Errorf("error removing uploaded file: %v", err)
|
t.Errorf("error removing uploaded file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1070,7 +1062,7 @@ func TestPasswordsHashPbkdf2Sha1(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Password = pbkdf2Pwd
|
u.Password = pbkdf2Pwd
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1090,7 +1082,7 @@ func TestPasswordsHashPbkdf2Sha1(t *testing.T) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("login with wrong password must fail")
|
t.Errorf("login with wrong password must fail")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1103,7 +1095,7 @@ func TestPasswordsHashPbkdf2Sha256(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Password = pbkdf2Pwd
|
u.Password = pbkdf2Pwd
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1123,7 +1115,7 @@ func TestPasswordsHashPbkdf2Sha256(t *testing.T) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("login with wrong password must fail")
|
t.Errorf("login with wrong password must fail")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1136,7 +1128,7 @@ func TestPasswordsHashPbkdf2Sha512(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Password = pbkdf2Pwd
|
u.Password = pbkdf2Pwd
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1156,7 +1148,7 @@ func TestPasswordsHashPbkdf2Sha512(t *testing.T) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("login with wrong password must fail")
|
t.Errorf("login with wrong password must fail")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1169,7 +1161,7 @@ func TestPasswordsHashBcrypt(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Password = bcryptPwd
|
u.Password = bcryptPwd
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1189,7 +1181,7 @@ func TestPasswordsHashBcrypt(t *testing.T) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("login with wrong password must fail")
|
t.Errorf("login with wrong password must fail")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1202,7 +1194,7 @@ func TestPasswordsHashSHA512Crypt(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Password = sha512CryptPwd
|
u.Password = sha512CryptPwd
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1222,7 +1214,7 @@ func TestPasswordsHashSHA512Crypt(t *testing.T) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("login with wrong password must fail")
|
t.Errorf("login with wrong password must fail")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1234,7 +1226,7 @@ func TestPermList(t *testing.T) {
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename,
|
u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename,
|
||||||
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1252,7 +1244,7 @@ func TestPermList(t *testing.T) {
|
||||||
t.Errorf("stat remote file without permission should not succeed")
|
t.Errorf("stat remote file without permission should not succeed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1264,7 +1256,7 @@ func TestPermDownload(t *testing.T) {
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename,
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermUpload, dataprovider.PermDelete, dataprovider.PermRename,
|
||||||
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1294,7 +1286,7 @@ func TestPermDownload(t *testing.T) {
|
||||||
t.Errorf("error removing uploaded file: %v", err)
|
t.Errorf("error removing uploaded file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1306,7 +1298,7 @@ func TestPermUpload(t *testing.T) {
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermDelete, dataprovider.PermRename,
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermDelete, dataprovider.PermRename,
|
||||||
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1327,7 +1319,7 @@ func TestPermUpload(t *testing.T) {
|
||||||
t.Errorf("file upload without permission should not succeed")
|
t.Errorf("file upload without permission should not succeed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1339,7 +1331,7 @@ func TestPermOverwrite(t *testing.T) {
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
||||||
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks}
|
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks}
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1364,7 +1356,7 @@ func TestPermOverwrite(t *testing.T) {
|
||||||
t.Errorf("file overwrite without permission should not succeed")
|
t.Errorf("file overwrite without permission should not succeed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1376,7 +1368,7 @@ func TestPermDelete(t *testing.T) {
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermRename,
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermRename,
|
||||||
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1401,7 +1393,7 @@ func TestPermDelete(t *testing.T) {
|
||||||
t.Errorf("delete without permission should not succeed")
|
t.Errorf("delete without permission should not succeed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1413,7 +1405,7 @@ func TestPermRename(t *testing.T) {
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
||||||
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
dataprovider.PermCreateDirs, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1442,7 +1434,7 @@ func TestPermRename(t *testing.T) {
|
||||||
t.Errorf("error removing uploaded file: %v", err)
|
t.Errorf("error removing uploaded file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1454,7 +1446,7 @@ func TestPermCreateDirs(t *testing.T) {
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
||||||
dataprovider.PermRename, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
dataprovider.PermRename, dataprovider.PermCreateSymlinks, dataprovider.PermOverwrite}
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1479,7 +1471,7 @@ func TestPermCreateDirs(t *testing.T) {
|
||||||
t.Errorf("mkdir without permission should not succeed")
|
t.Errorf("mkdir without permission should not succeed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1491,7 +1483,7 @@ func TestPermSymlink(t *testing.T) {
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
u.Permissions = []string{dataprovider.PermListItems, dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermDelete,
|
||||||
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermOverwrite}
|
dataprovider.PermRename, dataprovider.PermCreateDirs, dataprovider.PermOverwrite}
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1520,7 +1512,7 @@ func TestPermSymlink(t *testing.T) {
|
||||||
t.Errorf("error removing uploaded file: %v", err)
|
t.Errorf("error removing uploaded file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1529,7 +1521,7 @@ func TestPermSymlink(t *testing.T) {
|
||||||
|
|
||||||
func TestSSHConnection(t *testing.T) {
|
func TestSSHConnection(t *testing.T) {
|
||||||
usePubKey := false
|
usePubKey := false
|
||||||
user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1537,7 +1529,7 @@ func TestSSHConnection(t *testing.T) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("ssh connection must fail: %v", err)
|
t.Errorf("ssh connection must fail: %v", err)
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1551,7 +1543,7 @@ func TestSCPBasicHandling(t *testing.T) {
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.QuotaSize = 6553600
|
u.QuotaSize = 6553600
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1589,7 +1581,7 @@ func TestSCPBasicHandling(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
os.Remove(localPath)
|
os.Remove(localPath)
|
||||||
user, _, err = api.GetUserByID(user.ID, http.StatusOK)
|
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error getting user: %v", err)
|
t.Errorf("error getting user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1603,7 +1595,7 @@ func TestSCPBasicHandling(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded files")
|
t.Errorf("error removing uploaded files")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1616,7 +1608,7 @@ func TestSCPUploadFileOverwrite(t *testing.T) {
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.QuotaFiles = 1000
|
u.QuotaFiles = 1000
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1638,7 +1630,7 @@ func TestSCPUploadFileOverwrite(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error uploading existing file via scp: %v", err)
|
t.Errorf("error uploading existing file via scp: %v", err)
|
||||||
}
|
}
|
||||||
user, _, err = api.GetUserByID(user.ID, http.StatusOK)
|
user, _, err = httpd.GetUserByID(user.ID, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error getting user: %v", err)
|
t.Errorf("error getting user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1665,7 +1657,7 @@ func TestSCPUploadFileOverwrite(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded files")
|
t.Errorf("error removing uploaded files")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1677,7 +1669,7 @@ func TestSCPRecursive(t *testing.T) {
|
||||||
}
|
}
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1739,7 +1731,7 @@ func TestSCPRecursive(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded files")
|
t.Errorf("error removing uploaded files")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1752,7 +1744,7 @@ func TestSCPPermCreateDirs(t *testing.T) {
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermUpload}
|
u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermUpload}
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1788,7 +1780,7 @@ func TestSCPPermCreateDirs(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded files")
|
t.Errorf("error removing uploaded files")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1801,7 +1793,7 @@ func TestSCPPermUpload(t *testing.T) {
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermCreateDirs}
|
u.Permissions = []string{dataprovider.PermDownload, dataprovider.PermCreateDirs}
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1825,7 +1817,7 @@ func TestSCPPermUpload(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded files")
|
t.Errorf("error removing uploaded files")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1838,7 +1830,7 @@ func TestSCPPermOverwrite(t *testing.T) {
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}
|
u.Permissions = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1866,7 +1858,7 @@ func TestSCPPermOverwrite(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded files")
|
t.Errorf("error removing uploaded files")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1879,7 +1871,7 @@ func TestSCPPermDownload(t *testing.T) {
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Permissions = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}
|
u.Permissions = []string{dataprovider.PermUpload, dataprovider.PermCreateDirs}
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1909,7 +1901,7 @@ func TestSCPPermDownload(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded files")
|
t.Errorf("error removing uploaded files")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1924,7 +1916,7 @@ func TestSCPQuotaSize(t *testing.T) {
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.QuotaFiles = 1
|
u.QuotaFiles = 1
|
||||||
u.QuotaSize = testFileSize - 1
|
u.QuotaSize = testFileSize - 1
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1951,7 +1943,7 @@ func TestSCPQuotaSize(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded files")
|
t.Errorf("error removing uploaded files")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1962,7 +1954,7 @@ func TestSCPEscapeHomeDir(t *testing.T) {
|
||||||
t.Skip("scp command not found, unable to execute this test")
|
t.Skip("scp command not found, unable to execute this test")
|
||||||
}
|
}
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2004,7 +1996,7 @@ func TestSCPEscapeHomeDir(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded files")
|
t.Errorf("error removing uploaded files")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2015,7 +2007,7 @@ func TestSCPUploadPaths(t *testing.T) {
|
||||||
t.Skip("scp command not found, unable to execute this test")
|
t.Skip("scp command not found, unable to execute this test")
|
||||||
}
|
}
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2050,7 +2042,7 @@ func TestSCPUploadPaths(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded files")
|
t.Errorf("error removing uploaded files")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2061,7 +2053,7 @@ func TestSCPOverwriteDirWithFile(t *testing.T) {
|
||||||
t.Skip("scp command not found, unable to execute this test")
|
t.Skip("scp command not found, unable to execute this test")
|
||||||
}
|
}
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2083,7 +2075,7 @@ func TestSCPOverwriteDirWithFile(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded files")
|
t.Errorf("error removing uploaded files")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2094,14 +2086,14 @@ func TestSCPRemoteToRemote(t *testing.T) {
|
||||||
t.Skip("scp command not found, unable to execute this test")
|
t.Skip("scp command not found, unable to execute this test")
|
||||||
}
|
}
|
||||||
usePubKey := true
|
usePubKey := true
|
||||||
user, _, err := api.AddUser(getTestUser(usePubKey), http.StatusOK)
|
user, _, err := httpd.AddUser(getTestUser(usePubKey), http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
u := getTestUser(usePubKey)
|
u := getTestUser(usePubKey)
|
||||||
u.Username += "1"
|
u.Username += "1"
|
||||||
u.HomeDir += "1"
|
u.HomeDir += "1"
|
||||||
user1, _, err := api.AddUser(u, http.StatusOK)
|
user1, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2126,7 +2118,7 @@ func TestSCPRemoteToRemote(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded files")
|
t.Errorf("error removing uploaded files")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2134,7 +2126,7 @@ func TestSCPRemoteToRemote(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded files for user1")
|
t.Errorf("error removing uploaded files for user1")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user1, http.StatusOK)
|
_, err = httpd.RemoveUser(user1, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user1: %v", err)
|
t.Errorf("unable to remove user1: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2145,7 +2137,7 @@ func TestSCPErrors(t *testing.T) {
|
||||||
t.Skip("scp command not found, unable to execute this test")
|
t.Skip("scp command not found, unable to execute this test")
|
||||||
}
|
}
|
||||||
u := getTestUser(true)
|
u := getTestUser(true)
|
||||||
user, _, err := api.AddUser(u, http.StatusOK)
|
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to add user: %v", err)
|
t.Errorf("unable to add user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2165,7 +2157,7 @@ func TestSCPErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
user.UploadBandwidth = 512
|
user.UploadBandwidth = 512
|
||||||
user.DownloadBandwidth = 512
|
user.DownloadBandwidth = 512
|
||||||
_, _, err = api.UpdateUser(user, http.StatusOK)
|
_, _, err = httpd.UpdateUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to update user: %v", err)
|
t.Errorf("unable to update user: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -2201,7 +2193,7 @@ func TestSCPErrors(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("error removing uploaded files")
|
t.Errorf("error removing uploaded files")
|
||||||
}
|
}
|
||||||
_, err = api.RemoveUser(user, http.StatusOK)
|
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unable to remove user: %v", err)
|
t.Errorf("unable to remove user: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,8 @@
|
||||||
},
|
},
|
||||||
"httpd": {
|
"httpd": {
|
||||||
"bind_port": 8080,
|
"bind_port": 8080,
|
||||||
"bind_address": "127.0.0.1"
|
"bind_address": "127.0.0.1",
|
||||||
|
"templates_path": "templates",
|
||||||
|
"static_files_path": "static"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
20
static/css/fonts.css
Normal file
20
static/css/fonts.css
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('/static/vendor/fonts/Roboto-Bold-webfont.woff');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('/static/vendor/fonts/Roboto-Regular-webfont.woff');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('/static/vendor/fonts/Roboto-Light-webfont.woff');
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
10
static/css/sb-admin-2.min.css
vendored
Normal file
10
static/css/sb-admin-2.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
49
static/js/sb-admin-2.js
Normal file
49
static/js/sb-admin-2.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
(function($) {
|
||||||
|
"use strict"; // Start of use strict
|
||||||
|
|
||||||
|
// Toggle the side navigation
|
||||||
|
$("#sidebarToggle, #sidebarToggleTop").on('click', function(e) {
|
||||||
|
$("body").toggleClass("sidebar-toggled");
|
||||||
|
$(".sidebar").toggleClass("toggled");
|
||||||
|
if ($(".sidebar").hasClass("toggled")) {
|
||||||
|
$('.sidebar .collapse').collapse('hide');
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close any open menu accordions when window is resized below 768px
|
||||||
|
$(window).resize(function() {
|
||||||
|
if ($(window).width() < 768) {
|
||||||
|
$('.sidebar .collapse').collapse('hide');
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent the content wrapper from scrolling when the fixed side navigation hovered over
|
||||||
|
$('body.fixed-nav .sidebar').on('mousewheel DOMMouseScroll wheel', function(e) {
|
||||||
|
if ($(window).width() > 768) {
|
||||||
|
var e0 = e.originalEvent,
|
||||||
|
delta = e0.wheelDelta || -e0.detail;
|
||||||
|
this.scrollTop += (delta < 0 ? 1 : -1) * 30;
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scroll to top button appear
|
||||||
|
$(document).on('scroll', function() {
|
||||||
|
var scrollDistance = $(this).scrollTop();
|
||||||
|
if (scrollDistance > 100) {
|
||||||
|
$('.scroll-to-top').fadeIn();
|
||||||
|
} else {
|
||||||
|
$('.scroll-to-top').fadeOut();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Smooth scrolling using jQuery easing
|
||||||
|
$(document).on('click', 'a.scroll-to-top', function(e) {
|
||||||
|
var $anchor = $(this);
|
||||||
|
$('html, body').stop().animate({
|
||||||
|
scrollTop: ($($anchor.attr('href')).offset().top)
|
||||||
|
}, 1000, 'easeInOutExpo');
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
})(jQuery); // End of use strict
|
7
static/js/sb-admin-2.min.js
vendored
Normal file
7
static/js/sb-admin-2.min.js
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/*!
|
||||||
|
* Start Bootstrap - SB Admin 2 v4.0.7 (https://startbootstrap.com/template-overviews/sb-admin-2)
|
||||||
|
* Copyright 2013-2019 Start Bootstrap
|
||||||
|
* Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap-sb-admin-2/blob/master/LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
!function(t){"use strict";t("#sidebarToggle, #sidebarToggleTop").on("click",function(o){t("body").toggleClass("sidebar-toggled"),t(".sidebar").toggleClass("toggled"),t(".sidebar").hasClass("toggled")&&t(".sidebar .collapse").collapse("hide")}),t(window).resize(function(){t(window).width()<768&&t(".sidebar .collapse").collapse("hide")}),t("body.fixed-nav .sidebar").on("mousewheel DOMMouseScroll wheel",function(o){if(768<t(window).width()){var e=o.originalEvent,l=e.wheelDelta||-e.detail;this.scrollTop+=30*(l<0?1:-1),o.preventDefault()}}),t(document).on("scroll",function(){100<t(this).scrollTop()?t(".scroll-to-top").fadeIn():t(".scroll-to-top").fadeOut()}),t(document).on("click","a.scroll-to-top",function(o){var e=t(this);t("html, body").stop().animate({scrollTop:t(e.attr("href")).offset().top},1e3,"easeInOutExpo"),o.preventDefault()})}(jQuery);
|
7
static/vendor/bootstrap/js/bootstrap.bundle.min.js
vendored
Normal file
7
static/vendor/bootstrap/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
static/vendor/bootstrap/js/bootstrap.min.js
vendored
Normal file
7
static/vendor/bootstrap/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/vendor/datatables/buttons.bootstrap4.min.css
vendored
Normal file
1
static/vendor/datatables/buttons.bootstrap4.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
@keyframes dtb-spinner{100%{transform:rotate(360deg)}}@-o-keyframes dtb-spinner{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes dtb-spinner{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dtb-spinner{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes dtb-spinner{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border:2px solid #111;box-shadow:3px 3px 8px rgba(0,0,0,0.3);border-radius:3px;text-align:center;z-index:21}div.dt-button-info h2{padding:0.5em;margin:0;font-weight:normal;border-bottom:1px solid #ddd;background-color:#f3f3f3}div.dt-button-info>div{padding:1em}div.dt-button-collection-title{text-align:center;padding:0.3em 0 0.5em;font-size:0.9em}div.dt-button-collection-title:empty{display:none}div.dt-button-collection.dropdown-menu{display:block;z-index:2002}div.dt-button-collection.dropdown-menu.fixed{position:fixed;top:50%;left:50%;margin-left:-75px;border-radius:0}div.dt-button-collection.dropdown-menu.fixed.two-column{margin-left:-150px}div.dt-button-collection.dropdown-menu.fixed.three-column{margin-left:-225px}div.dt-button-collection.dropdown-menu.fixed.four-column{margin-left:-300px}div.dt-button-collection.dropdown-menu>div:last-child{-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection.dropdown-menu>div:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.dropdown-menu.two-column{width:300px}div.dt-button-collection.dropdown-menu.two-column>div:last-child{padding-bottom:1px;-webkit-column-count:2;-moz-column-count:2;-ms-column-count:2;-o-column-count:2;column-count:2}div.dt-button-collection.dropdown-menu.three-column{width:450px}div.dt-button-collection.dropdown-menu.three-column>div:last-child{padding-bottom:1px;-webkit-column-count:3;-moz-column-count:3;-ms-column-count:3;-o-column-count:3;column-count:3}div.dt-button-collection.dropdown-menu.four-column{width:600px}div.dt-button-collection.dropdown-menu.four-column>div:last-child{padding-bottom:1px;-webkit-column-count:4;-moz-column-count:4;-ms-column-count:4;-o-column-count:4;column-count:4}div.dt-button-collection.dropdown-menu .dt-button{border-radius:0}div.dt-button-collection.fixed{position:fixed;top:50%;left:50%;margin-left:-75px;border-radius:0}div.dt-button-collection.fixed.two-column{margin-left:-150px}div.dt-button-collection.fixed.three-column{margin-left:-225px}div.dt-button-collection.fixed.four-column{margin-left:-300px}div.dt-button-collection>div:last-child{-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection>div:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.two-column{width:300px}div.dt-button-collection.two-column>div:last-child{padding-bottom:1px;-webkit-column-count:2;-moz-column-count:2;-ms-column-count:2;-o-column-count:2;column-count:2}div.dt-button-collection.three-column{width:450px}div.dt-button-collection.three-column>div:last-child{padding-bottom:1px;-webkit-column-count:3;-moz-column-count:3;-ms-column-count:3;-o-column-count:3;column-count:3}div.dt-button-collection.four-column{width:600px}div.dt-button-collection.four-column>div:last-child{padding-bottom:1px;-webkit-column-count:4;-moz-column-count:4;-ms-column-count:4;-o-column-count:4;column-count:4}div.dt-button-collection .dt-button{border-radius:0}div.dt-button-collection.fixed{max-width:none}div.dt-button-collection.fixed:before,div.dt-button-collection.fixed:after{display:none}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;z-index:999}@media screen and (max-width: 767px){div.dt-buttons{float:none;width:100%;text-align:center;margin-bottom:0.5em}div.dt-buttons a.btn{float:none}}div.dt-buttons button.btn.processing,div.dt-buttons div.btn.processing,div.dt-buttons a.btn.processing{color:rgba(0,0,0,0.2)}div.dt-buttons button.btn.processing:after,div.dt-buttons div.btn.processing:after,div.dt-buttons a.btn.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:' ';border:2px solid #282828;border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear}
|
6
static/vendor/datatables/buttons.bootstrap4.min.js
vendored
Normal file
6
static/vendor/datatables/buttons.bootstrap4.min.js
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/*!
|
||||||
|
Bootstrap integration for DataTables' Buttons
|
||||||
|
©2016 SpryMedia Ltd - datatables.net/license
|
||||||
|
*/
|
||||||
|
(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-buttons"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);b&&b.fn.dataTable||(b=require("datatables.net-bs4")(a,b).$);b.fn.dataTable.Buttons||require("datatables.net-buttons")(a,b);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c,a,b,d){a=c.fn.dataTable;c.extend(!0,a.Buttons.defaults,{dom:{container:{className:"dt-buttons btn-group flex-wrap"},
|
||||||
|
button:{className:"btn btn-secondary"},collection:{tag:"div",className:"dt-button-collection dropdown-menu",button:{tag:"a",className:"dt-button dropdown-item",active:"active",disabled:"disabled"}}},buttonCreated:function(a,b){return a.buttons?c('<div class="btn-group"/>').append(b):b}});a.ext.buttons.collection.className+=" dropdown-toggle";a.ext.buttons.collection.rightAlignClassName="dropdown-menu-right";return a.Buttons});
|
1
static/vendor/datatables/dataTables.bootstrap4.min.css
vendored
Normal file
1
static/vendor/datatables/dataTables.bootstrap4.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
11
static/vendor/datatables/dataTables.bootstrap4.min.js
vendored
Normal file
11
static/vendor/datatables/dataTables.bootstrap4.min.js
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/*!
|
||||||
|
DataTables Bootstrap 4 integration
|
||||||
|
©2011-2017 SpryMedia Ltd - datatables.net/license
|
||||||
|
*/
|
||||||
|
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(a,b,c){a instanceof String&&(a=String(a));for(var e=a.length,d=0;d<e;d++){var k=a[d];if(b.call(c,k,d,a))return{i:d,v:k}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
|
||||||
|
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){a!=Array.prototype&&a!=Object.prototype&&(a[b]=c.value)};$jscomp.getGlobal=function(a){return"undefined"!=typeof window&&window===a?a:"undefined"!=typeof global&&null!=global?global:a};$jscomp.global=$jscomp.getGlobal(this);
|
||||||
|
$jscomp.polyfill=function(a,b,c,e){if(b){c=$jscomp.global;a=a.split(".");for(e=0;e<a.length-1;e++){var d=a[e];d in c||(c[d]={});c=c[d]}a=a[a.length-1];e=c[a];b=b(e);b!=e&&null!=b&&$jscomp.defineProperty(c,a,{configurable:!0,writable:!0,value:b})}};$jscomp.polyfill("Array.prototype.find",function(a){return a?a:function(a,c){return $jscomp.findInternal(this,a,c).v}},"es6","es3");
|
||||||
|
(function(a){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(b){return a(b,window,document)}):"object"===typeof exports?module.exports=function(b,c){b||(b=window);c&&c.fn.dataTable||(c=require("datatables.net")(b,c).$);return a(c,b,b.document)}:a(jQuery,window,document)})(function(a,b,c,e){var d=a.fn.dataTable;a.extend(!0,d.defaults,{dom:"<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>><'row'<'col-sm-12'tr>><'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||||
|
renderer:"bootstrap"});a.extend(d.ext.classes,{sWrapper:"dataTables_wrapper dt-bootstrap4",sFilterInput:"form-control form-control-sm",sLengthSelect:"custom-select custom-select-sm form-control form-control-sm",sProcessing:"dataTables_processing card",sPageButton:"paginate_button page-item"});d.ext.renderer.pageButton.bootstrap=function(b,l,v,w,m,r){var k=new d.Api(b),x=b.oClasses,n=b.oLanguage.oPaginate,y=b.oLanguage.oAria.paginate||{},g,h,t=0,u=function(c,d){var e,l=function(b){b.preventDefault();
|
||||||
|
a(b.currentTarget).hasClass("disabled")||k.page()==b.data.action||k.page(b.data.action).draw("page")};var q=0;for(e=d.length;q<e;q++){var f=d[q];if(a.isArray(f))u(c,f);else{h=g="";switch(f){case "ellipsis":g="…";h="disabled";break;case "first":g=n.sFirst;h=f+(0<m?"":" disabled");break;case "previous":g=n.sPrevious;h=f+(0<m?"":" disabled");break;case "next":g=n.sNext;h=f+(m<r-1?"":" disabled");break;case "last":g=n.sLast;h=f+(m<r-1?"":" disabled");break;default:g=f+1,h=m===f?"active":""}if(g){var p=
|
||||||
|
a("<li>",{"class":x.sPageButton+" "+h,id:0===v&&"string"===typeof f?b.sTableId+"_"+f:null}).append(a("<a>",{href:"#","aria-controls":b.sTableId,"aria-label":y[f],"data-dt-idx":t,tabindex:b.iTabIndex,"class":"page-link"}).html(g)).appendTo(c);b.oApi._fnBindAction(p,{action:f},l);t++}}}};try{var p=a(l).find(c.activeElement).data("dt-idx")}catch(z){}u(a(l).empty().html('<ul class="pagination"/>').children("ul"),w);p!==e&&a(l).find("[data-dt-idx="+p+"]").focus()};return d});
|
42
static/vendor/datatables/dataTables.buttons.min.js
vendored
Normal file
42
static/vendor/datatables/dataTables.buttons.min.js
vendored
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/*!
|
||||||
|
Buttons for DataTables 1.6.0
|
||||||
|
©2016-2019 SpryMedia Ltd - datatables.net/license
|
||||||
|
*/
|
||||||
|
(function(d){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(u){return d(u,window,document)}):"object"===typeof exports?module.exports=function(u,t){u||(u=window);t&&t.fn.dataTable||(t=require("datatables.net")(u,t).$);return d(t,u,u.document)}:d(jQuery,window,document)})(function(d,u,t,p){function y(a){a=new m.Api(a);var b=a.init().buttons||m.defaults.buttons;return(new n(a,b)).container()}var m=d.fn.dataTable,B=0,C=0,q=m.ext.buttons,n=function(a,b){if(!(this instanceof
|
||||||
|
n))return function(b){return(new n(b,a)).container()};"undefined"===typeof b&&(b={});!0===b&&(b={});d.isArray(b)&&(b={buttons:b});this.c=d.extend(!0,{},n.defaults,b);b.buttons&&(this.c.buttons=b.buttons);this.s={dt:new m.Api(a),buttons:[],listenKeys:"",namespace:"dtb"+B++};this.dom={container:d("<"+this.c.dom.container.tag+"/>").addClass(this.c.dom.container.className)};this._constructor()};d.extend(n.prototype,{action:function(a,b){a=this._nodeToButton(a);if(b===p)return a.conf.action;a.conf.action=
|
||||||
|
b;return this},active:function(a,b){var c=this._nodeToButton(a);a=this.c.dom.button.active;c=d(c.node);if(b===p)return c.hasClass(a);c.toggleClass(a,b===p?!0:b);return this},add:function(a,b){var c=this.s.buttons;if("string"===typeof b){b=b.split("-");var e=this.s;c=0;for(var d=b.length-1;c<d;c++)e=e.buttons[1*b[c]];c=e.buttons;b=1*b[b.length-1]}this._expandButton(c,a,e!==p,b);this._draw();return this},container:function(){return this.dom.container},disable:function(a){a=this._nodeToButton(a);d(a.node).addClass(this.c.dom.button.disabled);
|
||||||
|
return this},destroy:function(){d("body").off("keyup."+this.s.namespace);var a=this.s.buttons.slice(),b;var c=0;for(b=a.length;c<b;c++)this.remove(a[c].node);this.dom.container.remove();a=this.s.dt.settings()[0];c=0;for(b=a.length;c<b;c++)if(a.inst===this){a.splice(c,1);break}return this},enable:function(a,b){if(!1===b)return this.disable(a);a=this._nodeToButton(a);d(a.node).removeClass(this.c.dom.button.disabled);return this},name:function(){return this.c.name},node:function(a){if(!a)return this.dom.container;
|
||||||
|
a=this._nodeToButton(a);return d(a.node)},processing:function(a,b){var c=this.s.dt,e=this._nodeToButton(a);if(b===p)return d(e.node).hasClass("processing");d(e.node).toggleClass("processing",b);d(c.table().node()).triggerHandler("buttons-processing.dt",[b,c.button(a),c,d(a),e.conf]);return this},remove:function(a){var b=this._nodeToButton(a),c=this._nodeToHost(a),e=this.s.dt;if(b.buttons.length)for(var g=b.buttons.length-1;0<=g;g--)this.remove(b.buttons[g].node);b.conf.destroy&&b.conf.destroy.call(e.button(a),
|
||||||
|
e,d(a),b.conf);this._removeKey(b.conf);d(b.node).remove();a=d.inArray(b,c);c.splice(a,1);return this},text:function(a,b){var c=this._nodeToButton(a);a=this.c.dom.collection.buttonLiner;a=c.inCollection&&a&&a.tag?a.tag:this.c.dom.buttonLiner.tag;var e=this.s.dt,g=d(c.node),f=function(a){return"function"===typeof a?a(e,g,c.conf):a};if(b===p)return f(c.conf.text);c.conf.text=b;a?g.children(a).html(f(b)):g.html(f(b));return this},_constructor:function(){var a=this,b=this.s.dt,c=b.settings()[0],e=this.c.buttons;
|
||||||
|
c._buttons||(c._buttons=[]);c._buttons.push({inst:this,name:this.c.name});for(var g=0,f=e.length;g<f;g++)this.add(e[g]);b.on("destroy",function(b,e){e===c&&a.destroy()});d("body").on("keyup."+this.s.namespace,function(b){if(!t.activeElement||t.activeElement===t.body){var c=String.fromCharCode(b.keyCode).toLowerCase();-1!==a.s.listenKeys.toLowerCase().indexOf(c)&&a._keypress(c,b)}})},_addKey:function(a){a.key&&(this.s.listenKeys+=d.isPlainObject(a.key)?a.key.key:a.key)},_draw:function(a,b){a||(a=this.dom.container,
|
||||||
|
b=this.s.buttons);a.children().detach();for(var c=0,e=b.length;c<e;c++)a.append(b[c].inserter),a.append(" "),b[c].buttons&&b[c].buttons.length&&this._draw(b[c].collection,b[c].buttons)},_expandButton:function(a,b,c,e){var g=this.s.dt,f=0;b=d.isArray(b)?b:[b];for(var h=0,k=b.length;h<k;h++){var r=this._resolveExtends(b[h]);if(r)if(d.isArray(r))this._expandButton(a,r,c,e);else{var l=this._buildButton(r,c);l&&(e!==p?(a.splice(e,0,l),e++):a.push(l),l.conf.buttons&&(l.collection=d("<div/>"),l.conf._collection=
|
||||||
|
l.collection,this._expandButton(l.buttons,l.conf.buttons,!0,e)),r.init&&r.init.call(g.button(l.node),g,d(l.node),r),f++)}}},_buildButton:function(a,b){var c=this.c.dom.button,e=this.c.dom.buttonLiner,g=this.c.dom.collection,f=this.s.dt,h=function(b){return"function"===typeof b?b(f,l,a):b};b&&g.button&&(c=g.button);b&&g.buttonLiner&&(e=g.buttonLiner);if(a.available&&!a.available(f,a))return!1;var k=function(a,b,c,e){e.action.call(b.button(c),a,b,c,e);d(b.table().node()).triggerHandler("buttons-action.dt",
|
||||||
|
[b.button(c),b,c,e])};g=a.tag||c.tag;var r=a.clickBlurs===p?!0:a.clickBlurs,l=d("<"+g+"/>").addClass(c.className).attr("tabindex",this.s.dt.settings()[0].iTabIndex).attr("aria-controls",this.s.dt.table().node().id).on("click.dtb",function(b){b.preventDefault();!l.hasClass(c.disabled)&&a.action&&k(b,f,l,a);r&&l.blur()}).on("keyup.dtb",function(b){13===b.keyCode&&!l.hasClass(c.disabled)&&a.action&&k(b,f,l,a)});"a"===g.toLowerCase()&&l.attr("href","#");"button"===g.toLowerCase()&&l.attr("type","button");
|
||||||
|
e.tag?(g=d("<"+e.tag+"/>").html(h(a.text)).addClass(e.className),"a"===e.tag.toLowerCase()&&g.attr("href","#"),l.append(g)):l.html(h(a.text));!1===a.enabled&&l.addClass(c.disabled);a.className&&l.addClass(a.className);a.titleAttr&&l.attr("title",h(a.titleAttr));a.attr&&l.attr(a.attr);a.namespace||(a.namespace=".dt-button-"+C++);e=(e=this.c.dom.buttonContainer)&&e.tag?d("<"+e.tag+"/>").addClass(e.className).append(l):l;this._addKey(a);this.c.buttonCreated&&(e=this.c.buttonCreated(a,e));return{conf:a,
|
||||||
|
node:l.get(0),inserter:e,buttons:[],inCollection:b,collection:null}},_nodeToButton:function(a,b){b||(b=this.s.buttons);for(var c=0,e=b.length;c<e;c++){if(b[c].node===a)return b[c];if(b[c].buttons.length){var d=this._nodeToButton(a,b[c].buttons);if(d)return d}}},_nodeToHost:function(a,b){b||(b=this.s.buttons);for(var c=0,e=b.length;c<e;c++){if(b[c].node===a)return b;if(b[c].buttons.length){var d=this._nodeToHost(a,b[c].buttons);if(d)return d}}},_keypress:function(a,b){if(!b._buttonsHandled){var c=
|
||||||
|
function(e){for(var g=0,f=e.length;g<f;g++){var h=e[g].conf,k=e[g].node;h.key&&(h.key===a?(b._buttonsHandled=!0,d(k).click()):!d.isPlainObject(h.key)||h.key.key!==a||h.key.shiftKey&&!b.shiftKey||h.key.altKey&&!b.altKey||h.key.ctrlKey&&!b.ctrlKey||h.key.metaKey&&!b.metaKey||(b._buttonsHandled=!0,d(k).click()));e[g].buttons.length&&c(e[g].buttons)}};c(this.s.buttons)}},_removeKey:function(a){if(a.key){var b=d.isPlainObject(a.key)?a.key.key:a.key;a=this.s.listenKeys.split("");b=d.inArray(b,a);a.splice(b,
|
||||||
|
1);this.s.listenKeys=a.join("")}},_resolveExtends:function(a){var b=this.s.dt,c,e=function(c){for(var e=0;!d.isPlainObject(c)&&!d.isArray(c);){if(c===p)return;if("function"===typeof c){if(c=c(b,a),!c)return!1}else if("string"===typeof c){if(!q[c])throw"Unknown button type: "+c;c=q[c]}e++;if(30<e)throw"Buttons: Too many iterations";}return d.isArray(c)?c:d.extend({},c)};for(a=e(a);a&&a.extend;){if(!q[a.extend])throw"Cannot extend unknown button type: "+a.extend;var g=e(q[a.extend]);if(d.isArray(g))return g;
|
||||||
|
if(!g)return!1;var f=g.className;a=d.extend({},g,a);f&&a.className!==f&&(a.className=f+" "+a.className);var h=a.postfixButtons;if(h){a.buttons||(a.buttons=[]);f=0;for(c=h.length;f<c;f++)a.buttons.push(h[f]);a.postfixButtons=null}if(h=a.prefixButtons){a.buttons||(a.buttons=[]);f=0;for(c=h.length;f<c;f++)a.buttons.splice(f,0,h[f]);a.prefixButtons=null}a.extend=g.extend}return a},_popover:function(a,b,c){var e=this.c,g=d.extend({align:"button-left",autoClose:!1,background:!0,backgroundClassName:"dt-button-background",
|
||||||
|
contentClassName:e.dom.collection.className,collectionLayout:"",collectionTitle:"",dropup:!1,fade:400,rightAlignClassName:"dt-button-right",tag:e.dom.collection.tag},c),f=b.node(),h=function(){d(".dt-button-collection").stop().fadeOut(g.fade,function(){d(this).detach()});d(b.buttons('[aria-haspopup="true"][aria-expanded="true"]').nodes()).attr("aria-expanded","false");d("div.dt-button-background").off("click.dtb-collection");n.background(!1,g.backgroundClassName,g.fade,f);d("body").off(".dtb-collection");
|
||||||
|
b.off("buttons-action.b-internal")};!1===a&&h();c=d(b.buttons('[aria-haspopup="true"][aria-expanded="true"]').nodes());c.length&&(f=c.eq(0),h());a=d(a);var k=d(b.table().container());f.attr("aria-expanded","true");f.parents("body")[0]!==t.body&&(f=t.body.lastChild);c=d("<"+g.tag+"/>").addClass(g.contentClassName).attr("role","menu");g.collectionTitle&&c.prepend('<div class="dt-button-collection-title">'+g.collectionTitle+"</div>");c.addClass(g.collectionLayout).css("display","none").append(a).insertAfter(f).stop().fadeIn(g.fade);
|
||||||
|
e=c.css("position");"dt-container"===g.align&&(f=f.parent(),c.css("width",k.width()));if("absolute"===e){e=f.position();c.css({top:e.top+f.outerHeight(),left:e.left});var r=c.outerHeight(),l=c.outerWidth(),w=k.offset().top+k.height();w=e.top+f.outerHeight()+r-w;var D=e.top-r,m=k.offset().top;r=e.top-r-5;(w>m-D||g.dropup)&&-r<m&&c.css("top",r);(c.hasClass(g.rightAlignClassName)||"button-right"===g.align)&&c.css("left",e.left+f.outerWidth()-l);r=e.left+l;k=k.offset().left+k.width();r>k&&c.css("left",
|
||||||
|
e.left-(r-k));k=f.offset().left+l;k>d(u).width()&&c.css("left",e.left-(k-d(u).width()))}else e=c.height()/2,e>d(u).height()/2&&(e=d(u).height()/2),c.css("marginTop",-1*e);g.background&&n.background(!0,g.backgroundClassName,g.fade,f);d("div.dt-button-background").on("click.dtb-collection",function(){});d("body").on("click.dtb-collection",function(b){var c=d.fn.addBack?"addBack":"andSelf";d(b.target).parents()[c]().filter(a).length||h()}).on("keyup.dtb-collection",function(a){27===a.keyCode&&h()});
|
||||||
|
g.autoClose&&setTimeout(function(){b.on("buttons-action.b-internal",function(a,b,c,e){e[0]!==f[0]&&h()})},0)}});n.background=function(a,b,c,e){c===p&&(c=400);e||(e=t.body);a?d("<div/>").addClass(b).css("display","none").insertAfter(e).stop().fadeIn(c):d("div."+b).stop().fadeOut(c,function(){d(this).removeClass(b).remove()})};n.instanceSelector=function(a,b){if(a===p||null===a)return d.map(b,function(a){return a.inst});var c=[],e=d.map(b,function(a){return a.name}),g=function(a){if(d.isArray(a))for(var f=
|
||||||
|
0,k=a.length;f<k;f++)g(a[f]);else"string"===typeof a?-1!==a.indexOf(",")?g(a.split(",")):(a=d.inArray(d.trim(a),e),-1!==a&&c.push(b[a].inst)):"number"===typeof a&&c.push(b[a].inst)};g(a);return c};n.buttonSelector=function(a,b){for(var c=[],e=function(a,b,c){for(var d,g,f=0,k=b.length;f<k;f++)if(d=b[f])g=c!==p?c+f:f+"",a.push({node:d.node,name:d.conf.name,idx:g}),d.buttons&&e(a,d.buttons,g+"-")},g=function(a,b){var f,h=[];e(h,b.s.buttons);var k=d.map(h,function(a){return a.node});if(d.isArray(a)||
|
||||||
|
a instanceof d)for(k=0,f=a.length;k<f;k++)g(a[k],b);else if(null===a||a===p||"*"===a)for(k=0,f=h.length;k<f;k++)c.push({inst:b,node:h[k].node});else if("number"===typeof a)c.push({inst:b,node:b.s.buttons[a].node});else if("string"===typeof a)if(-1!==a.indexOf(","))for(h=a.split(","),k=0,f=h.length;k<f;k++)g(d.trim(h[k]),b);else if(a.match(/^\d+(\-\d+)*$/))k=d.map(h,function(a){return a.idx}),c.push({inst:b,node:h[d.inArray(a,k)].node});else if(-1!==a.indexOf(":name"))for(a=a.replace(":name",""),k=
|
||||||
|
0,f=h.length;k<f;k++)h[k].name===a&&c.push({inst:b,node:h[k].node});else d(k).filter(a).each(function(){c.push({inst:b,node:this})});else"object"===typeof a&&a.nodeName&&(h=d.inArray(a,k),-1!==h&&c.push({inst:b,node:k[h]}))},f=0,h=a.length;f<h;f++)g(b,a[f]);return c};n.defaults={buttons:["copy","excel","csv","pdf","print"],name:"main",tabIndex:0,dom:{container:{tag:"div",className:"dt-buttons"},collection:{tag:"div",className:"dt-button-collection"},button:{tag:"ActiveXObject"in u?"a":"button",className:"dt-button",
|
||||||
|
active:"active",disabled:"disabled"},buttonLiner:{tag:"span",className:""}}};n.version="1.6.0";d.extend(q,{collection:{text:function(a){return a.i18n("buttons.collection","Collection")},className:"buttons-collection",init:function(a,b,c){b.attr("aria-expanded",!1)},action:function(a,b,c,e){a.stopPropagation();e._collection.parents("body").length?this.popover(!1,e):this.popover(e._collection,e)},attr:{"aria-haspopup":!0}},copy:function(a,b){if(q.copyHtml5)return"copyHtml5";if(q.copyFlash&&q.copyFlash.available(a,
|
||||||
|
b))return"copyFlash"},csv:function(a,b){if(q.csvHtml5&&q.csvHtml5.available(a,b))return"csvHtml5";if(q.csvFlash&&q.csvFlash.available(a,b))return"csvFlash"},excel:function(a,b){if(q.excelHtml5&&q.excelHtml5.available(a,b))return"excelHtml5";if(q.excelFlash&&q.excelFlash.available(a,b))return"excelFlash"},pdf:function(a,b){if(q.pdfHtml5&&q.pdfHtml5.available(a,b))return"pdfHtml5";if(q.pdfFlash&&q.pdfFlash.available(a,b))return"pdfFlash"},pageLength:function(a){a=a.settings()[0].aLengthMenu;var b=d.isArray(a[0])?
|
||||||
|
a[0]:a,c=d.isArray(a[0])?a[1]:a;return{extend:"collection",text:function(a){return a.i18n("buttons.pageLength",{"-1":"Show all rows",_:"Show %d rows"},a.page.len())},className:"buttons-page-length",autoClose:!0,buttons:d.map(b,function(a,b){return{text:c[b],className:"button-page-length",action:function(b,c){c.page.len(a).draw()},init:function(b,c,d){var e=this;c=function(){e.active(b.page.len()===a)};b.on("length.dt"+d.namespace,c);c()},destroy:function(a,b,c){a.off("length.dt"+c.namespace)}}}),
|
||||||
|
init:function(a,b,c){var d=this;a.on("length.dt"+c.namespace,function(){d.text(c.text)})},destroy:function(a,b,c){a.off("length.dt"+c.namespace)}}}});m.Api.register("buttons()",function(a,b){b===p&&(b=a,a=p);this.selector.buttonGroup=a;var c=this.iterator(!0,"table",function(c){if(c._buttons)return n.buttonSelector(n.instanceSelector(a,c._buttons),b)},!0);c._groupSelector=a;return c});m.Api.register("button()",function(a,b){a=this.buttons(a,b);1<a.length&&a.splice(1,a.length);return a});m.Api.registerPlural("buttons().active()",
|
||||||
|
"button().active()",function(a){return a===p?this.map(function(a){return a.inst.active(a.node)}):this.each(function(b){b.inst.active(b.node,a)})});m.Api.registerPlural("buttons().action()","button().action()",function(a){return a===p?this.map(function(a){return a.inst.action(a.node)}):this.each(function(b){b.inst.action(b.node,a)})});m.Api.register(["buttons().enable()","button().enable()"],function(a){return this.each(function(b){b.inst.enable(b.node,a)})});m.Api.register(["buttons().disable()",
|
||||||
|
"button().disable()"],function(){return this.each(function(a){a.inst.disable(a.node)})});m.Api.registerPlural("buttons().nodes()","button().node()",function(){var a=d();d(this.each(function(b){a=a.add(b.inst.node(b.node))}));return a});m.Api.registerPlural("buttons().processing()","button().processing()",function(a){return a===p?this.map(function(a){return a.inst.processing(a.node)}):this.each(function(b){b.inst.processing(b.node,a)})});m.Api.registerPlural("buttons().text()","button().text()",function(a){return a===
|
||||||
|
p?this.map(function(a){return a.inst.text(a.node)}):this.each(function(b){b.inst.text(b.node,a)})});m.Api.registerPlural("buttons().trigger()","button().trigger()",function(){return this.each(function(a){a.inst.node(a.node).trigger("click")})});m.Api.register("button().popover()",function(a,b){return this.map(function(c){return c.inst._popover(a,this.button(this[0].node),b)})});m.Api.register("buttons().containers()",function(){var a=d(),b=this._groupSelector;this.iterator(!0,"table",function(c){if(c._buttons){c=
|
||||||
|
n.instanceSelector(b,c._buttons);for(var d=0,g=c.length;d<g;d++)a=a.add(c[d].container())}});return a});m.Api.register("buttons().container()",function(){return this.containers().eq(0)});m.Api.register("button().add()",function(a,b){var c=this.context;c.length&&(c=n.instanceSelector(this._groupSelector,c[0]._buttons),c.length&&c[0].add(b,a));return this.button(this._groupSelector,a)});m.Api.register("buttons().destroy()",function(){this.pluck("inst").unique().each(function(a){a.destroy()});return this});
|
||||||
|
m.Api.registerPlural("buttons().remove()","buttons().remove()",function(){this.each(function(a){a.inst.remove(a.node)});return this});var v;m.Api.register("buttons.info()",function(a,b,c){var e=this;if(!1===a)return this.off("destroy.btn-info"),d("#datatables_buttons_info").fadeOut(function(){d(this).remove()}),clearTimeout(v),v=null,this;v&&clearTimeout(v);d("#datatables_buttons_info").length&&d("#datatables_buttons_info").remove();a=a?"<h2>"+a+"</h2>":"";d('<div id="datatables_buttons_info" class="dt-button-info"/>').html(a).append(d("<div/>")["string"===
|
||||||
|
typeof b?"html":"append"](b)).css("display","none").appendTo("body").fadeIn();c!==p&&0!==c&&(v=setTimeout(function(){e.buttons.info(!1)},c));this.on("destroy.btn-info",function(){e.buttons.info(!1)});return this});m.Api.register("buttons.exportData()",function(a){if(this.context.length)return E(new m.Api(this.context[0]),a)});m.Api.register("buttons.exportInfo()",function(a){a||(a={});var b=a;var c="*"===b.filename&&"*"!==b.title&&b.title!==p&&null!==b.title&&""!==b.title?b.title:b.filename;"function"===
|
||||||
|
typeof c&&(c=c());c===p||null===c?c=null:(-1!==c.indexOf("*")&&(c=d.trim(c.replace("*",d("head > title").text()))),c=c.replace(/[^a-zA-Z0-9_\u00A1-\uFFFF\.,\-_ !\(\)]/g,""),(b=x(b.extension))||(b=""),c+=b);b=x(a.title);b=null===b?null:-1!==b.indexOf("*")?b.replace("*",d("head > title").text()||"Exported data"):b;return{filename:c,title:b,messageTop:z(this,a.message||a.messageTop,"top"),messageBottom:z(this,a.messageBottom,"bottom")}});var x=function(a){return null===a||a===p?null:"function"===typeof a?
|
||||||
|
a():a},z=function(a,b,c){b=x(b);if(null===b)return null;a=d("caption",a.table().container()).eq(0);return"*"===b?a.css("caption-side")!==c?null:a.length?a.text():"":b},A=d("<textarea/>")[0],E=function(a,b){var c=d.extend(!0,{},{rows:null,columns:"",modifier:{search:"applied",order:"applied"},orthogonal:"display",stripHtml:!0,stripNewlines:!0,decodeEntities:!0,trim:!0,format:{header:function(a){return e(a)},footer:function(a){return e(a)},body:function(a){return e(a)}},customizeData:null},b),e=function(a){if("string"!==
|
||||||
|
typeof a)return a;a=a.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,"");a=a.replace(/<!\-\-.*?\-\->/g,"");c.stripHtml&&(a=a.replace(/<[^>]*>/g,""));c.trim&&(a=a.replace(/^\s+|\s+$/g,""));c.stripNewlines&&(a=a.replace(/\n/g," "));c.decodeEntities&&(A.innerHTML=a,a=A.value);return a};b=a.columns(c.columns).indexes().map(function(b){var d=a.column(b).header();return c.format.header(d.innerHTML,b,d)}).toArray();var g=a.table().footer()?a.columns(c.columns).indexes().map(function(b){var d=
|
||||||
|
a.column(b).footer();return c.format.footer(d?d.innerHTML:"",b,d)}).toArray():null,f=d.extend({},c.modifier);a.select&&"function"===typeof a.select.info&&f.selected===p&&a.rows(c.rows,d.extend({selected:!0},f)).any()&&d.extend(f,{selected:!0});f=a.rows(c.rows,f).indexes().toArray();var h=a.cells(f,c.columns);f=h.render(c.orthogonal).toArray();h=h.nodes().toArray();for(var k=b.length,m=[],l=0,n=0,q=0<k?f.length/k:0;n<q;n++){for(var u=[k],t=0;t<k;t++)u[t]=c.format.body(f[l],n,t,h[l]),l++;m[n]=u}b={header:b,
|
||||||
|
footer:g,body:m};c.customizeData&&c.customizeData(b);return b};d.fn.dataTable.Buttons=n;d.fn.DataTable.Buttons=n;d(t).on("init.dt plugin-init.dt",function(a,b){"dt"===a.namespace&&(a=b.oInit.buttons||m.defaults.buttons)&&!b._buttons&&(new n(b,a)).container()});m.ext.feature.push({fnInit:y,cFeature:"B"});m.ext.features&&m.ext.features.register("buttons",y);return n});
|
38
static/vendor/datatables/dataTables.select.min.js
vendored
Normal file
38
static/vendor/datatables/dataTables.select.min.js
vendored
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*!
|
||||||
|
Copyright 2015-2019 SpryMedia Ltd.
|
||||||
|
|
||||||
|
This source file is free software, available under the following license:
|
||||||
|
MIT license - http://datatables.net/license/mit
|
||||||
|
|
||||||
|
This source file is distributed in the hope that it will be useful, but
|
||||||
|
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
|
||||||
|
|
||||||
|
For details please refer to: http://www.datatables.net/extensions/select
|
||||||
|
Select for DataTables 1.3.1
|
||||||
|
2015-2019 SpryMedia Ltd - datatables.net/license/mit
|
||||||
|
*/
|
||||||
|
(function(f){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(k){return f(k,window,document)}):"object"===typeof exports?module.exports=function(k,p){k||(k=window);p&&p.fn.dataTable||(p=require("datatables.net")(k,p).$);return f(p,k,k.document)}:f(jQuery,window,document)})(function(f,k,p,h){function z(a,b,c){var d=function(c,b){if(c>b){var d=b;b=c;c=d}var e=!1;return a.columns(":visible").indexes().filter(function(a){a===c&&(e=!0);return a===b?(e=!1,!0):e})};var e=
|
||||||
|
function(c,b){var d=a.rows({search:"applied"}).indexes();if(d.indexOf(c)>d.indexOf(b)){var e=b;b=c;c=e}var f=!1;return d.filter(function(a){a===c&&(f=!0);return a===b?(f=!1,!0):f})};a.cells({selected:!0}).any()||c?(d=d(c.column,b.column),c=e(c.row,b.row)):(d=d(0,b.column),c=e(0,b.row));c=a.cells(c,d).flatten();a.cells(b,{selected:!0}).any()?a.cells(c).deselect():a.cells(c).select()}function v(a){var b=a.settings()[0]._select.selector;f(a.table().container()).off("mousedown.dtSelect",b).off("mouseup.dtSelect",
|
||||||
|
b).off("click.dtSelect",b);f("body").off("click.dtSelect"+a.table().node().id.replace(/[^a-zA-Z0-9\-_]/g,"-"))}function A(a){var b=f(a.table().container()),c=a.settings()[0],d=c._select.selector,e;b.on("mousedown.dtSelect",d,function(a){if(a.shiftKey||a.metaKey||a.ctrlKey)b.css("-moz-user-select","none").one("selectstart.dtSelect",d,function(){return!1});k.getSelection&&(e=k.getSelection())}).on("mouseup.dtSelect",d,function(){b.css("-moz-user-select","")}).on("click.dtSelect",d,function(c){var b=
|
||||||
|
a.select.items();if(e){var d=k.getSelection();if((!d.anchorNode||f(d.anchorNode).closest("table")[0]===a.table().node())&&d!==e)return}d=a.settings()[0];var l=f.trim(a.settings()[0].oClasses.sWrapper).replace(/ +/g,".");if(f(c.target).closest("div."+l)[0]==a.table().container()&&(l=a.cell(f(c.target).closest("td, th")),l.any())){var g=f.Event("user-select.dt");m(a,g,[b,l,c]);g.isDefaultPrevented()||(g=l.index(),"row"===b?(b=g.row,w(c,a,d,"row",b)):"column"===b?(b=l.index().column,w(c,a,d,"column",
|
||||||
|
b)):"cell"===b&&(b=l.index(),w(c,a,d,"cell",b)),d._select_lastCell=g)}});f("body").on("click.dtSelect"+a.table().node().id.replace(/[^a-zA-Z0-9\-_]/g,"-"),function(b){!c._select.blurable||f(b.target).parents().filter(a.table().container()).length||0===f(b.target).parents("html").length||f(b.target).parents("div.DTE").length||r(c,!0)})}function m(a,b,c,d){if(!d||a.flatten().length)"string"===typeof b&&(b+=".dt"),c.unshift(a),f(a.table().node()).trigger(b,c)}function B(a){var b=a.settings()[0];if(b._select.info&&
|
||||||
|
b.aanFeatures.i&&"api"!==a.select.style()){var c=a.rows({selected:!0}).flatten().length,d=a.columns({selected:!0}).flatten().length,e=a.cells({selected:!0}).flatten().length,l=function(b,c,d){b.append(f('<span class="select-item"/>').append(a.i18n("select."+c+"s",{_:"%d "+c+"s selected",0:"",1:"1 "+c+" selected"},d)))};f.each(b.aanFeatures.i,function(b,a){a=f(a);b=f('<span class="select-info"/>');l(b,"row",c);l(b,"column",d);l(b,"cell",e);var g=a.children("span.select-info");g.length&&g.remove();
|
||||||
|
""!==b.text()&&a.append(b)})}}function D(a){var b=new g.Api(a);a.aoRowCreatedCallback.push({fn:function(b,d,e){d=a.aoData[e];d._select_selected&&f(b).addClass(a._select.className);b=0;for(e=a.aoColumns.length;b<e;b++)(a.aoColumns[b]._select_selected||d._selected_cells&&d._selected_cells[b])&&f(d.anCells[b]).addClass(a._select.className)},sName:"select-deferRender"});b.on("preXhr.dt.dtSelect",function(){var a=b.rows({selected:!0}).ids(!0).filter(function(b){return b!==h}),d=b.cells({selected:!0}).eq(0).map(function(a){var c=
|
||||||
|
b.row(a.row).id(!0);return c?{row:c,column:a.column}:h}).filter(function(b){return b!==h});b.one("draw.dt.dtSelect",function(){b.rows(a).select();d.any()&&d.each(function(a){b.cells(a.row,a.column).select()})})});b.on("draw.dtSelect.dt select.dtSelect.dt deselect.dtSelect.dt info.dt",function(){B(b)});b.on("destroy.dtSelect",function(){v(b);b.off(".dtSelect")})}function C(a,b,c,d){var e=a[b+"s"]({search:"applied"}).indexes();d=f.inArray(d,e);var g=f.inArray(c,e);if(a[b+"s"]({selected:!0}).any()||
|
||||||
|
-1!==d){if(d>g){var u=g;g=d;d=u}e.splice(g+1,e.length);e.splice(0,d)}else e.splice(f.inArray(c,e)+1,e.length);a[b](c,{selected:!0}).any()?(e.splice(f.inArray(c,e),1),a[b+"s"](e).deselect()):a[b+"s"](e).select()}function r(a,b){if(b||"single"===a._select.style)a=new g.Api(a),a.rows({selected:!0}).deselect(),a.columns({selected:!0}).deselect(),a.cells({selected:!0}).deselect()}function w(a,b,c,d,e){var f=b.select.style(),g=b.select.toggleable(),h=b[d](e,{selected:!0}).any();if(!h||g)"os"===f?a.ctrlKey||
|
||||||
|
a.metaKey?b[d](e).select(!h):a.shiftKey?"cell"===d?z(b,e,c._select_lastCell||null):C(b,d,e,c._select_lastCell?c._select_lastCell[d]:null):(a=b[d+"s"]({selected:!0}),h&&1===a.flatten().length?b[d](e).deselect():(a.deselect(),b[d](e).select())):"multi+shift"==f?a.shiftKey?"cell"===d?z(b,e,c._select_lastCell||null):C(b,d,e,c._select_lastCell?c._select_lastCell[d]:null):b[d](e).select(!h):b[d](e).select(!h)}function t(a,b){return function(c){return c.i18n("buttons."+a,b)}}function x(a){a=a._eventNamespace;
|
||||||
|
return"draw.dt.DT"+a+" select.dt.DT"+a+" deselect.dt.DT"+a}function E(a,b){return-1!==f.inArray("rows",b.limitTo)&&a.rows({selected:!0}).any()||-1!==f.inArray("columns",b.limitTo)&&a.columns({selected:!0}).any()||-1!==f.inArray("cells",b.limitTo)&&a.cells({selected:!0}).any()?!0:!1}var g=f.fn.dataTable;g.select={};g.select.version="1.3.1";g.select.init=function(a){var b=a.settings()[0],c=b.oInit.select,d=g.defaults.select;c=c===h?d:c;d="row";var e="api",l=!1,u=!0,k=!0,m="td, th",p="selected",n=!1;
|
||||||
|
b._select={};!0===c?(e="os",n=!0):"string"===typeof c?(e=c,n=!0):f.isPlainObject(c)&&(c.blurable!==h&&(l=c.blurable),c.toggleable!==h&&(u=c.toggleable),c.info!==h&&(k=c.info),c.items!==h&&(d=c.items),e=c.style!==h?c.style:"os",n=!0,c.selector!==h&&(m=c.selector),c.className!==h&&(p=c.className));a.select.selector(m);a.select.items(d);a.select.style(e);a.select.blurable(l);a.select.toggleable(u);a.select.info(k);b._select.className=p;f.fn.dataTable.ext.order["select-checkbox"]=function(b,a){return this.api().column(a,
|
||||||
|
{order:"index"}).nodes().map(function(a){return"row"===b._select.items?f(a).parent().hasClass(b._select.className):"cell"===b._select.items?f(a).hasClass(b._select.className):!1})};!n&&f(a.table().node()).hasClass("selectable")&&a.select.style("os")};f.each([{type:"row",prop:"aoData"},{type:"column",prop:"aoColumns"}],function(a,b){g.ext.selector[b.type].push(function(a,d,e){d=d.selected;var c=[];if(!0!==d&&!1!==d)return e;for(var f=0,g=e.length;f<g;f++){var h=a[b.prop][e[f]];(!0===d&&!0===h._select_selected||
|
||||||
|
!1===d&&!h._select_selected)&&c.push(e[f])}return c})});g.ext.selector.cell.push(function(a,b,c){b=b.selected;var d=[];if(b===h)return c;for(var e=0,f=c.length;e<f;e++){var g=a.aoData[c[e].row];(!0===b&&g._selected_cells&&!0===g._selected_cells[c[e].column]||!(!1!==b||g._selected_cells&&g._selected_cells[c[e].column]))&&d.push(c[e])}return d});var n=g.Api.register,q=g.Api.registerPlural;n("select()",function(){return this.iterator("table",function(a){g.select.init(new g.Api(a))})});n("select.blurable()",
|
||||||
|
function(a){return a===h?this.context[0]._select.blurable:this.iterator("table",function(b){b._select.blurable=a})});n("select.toggleable()",function(a){return a===h?this.context[0]._select.toggleable:this.iterator("table",function(b){b._select.toggleable=a})});n("select.info()",function(a){return B===h?this.context[0]._select.info:this.iterator("table",function(b){b._select.info=a})});n("select.items()",function(a){return a===h?this.context[0]._select.items:this.iterator("table",function(b){b._select.items=
|
||||||
|
a;m(new g.Api(b),"selectItems",[a])})});n("select.style()",function(a){return a===h?this.context[0]._select.style:this.iterator("table",function(b){b._select.style=a;b._select_init||D(b);var c=new g.Api(b);v(c);"api"!==a&&A(c);m(new g.Api(b),"selectStyle",[a])})});n("select.selector()",function(a){return a===h?this.context[0]._select.selector:this.iterator("table",function(b){v(new g.Api(b));b._select.selector=a;"api"!==b._select.style&&A(new g.Api(b))})});q("rows().select()","row().select()",function(a){var b=
|
||||||
|
this;if(!1===a)return this.deselect();this.iterator("row",function(b,a){r(b);b.aoData[a]._select_selected=!0;f(b.aoData[a].nTr).addClass(b._select.className)});this.iterator("table",function(a,d){m(b,"select",["row",b[d]],!0)});return this});q("columns().select()","column().select()",function(a){var b=this;if(!1===a)return this.deselect();this.iterator("column",function(b,a){r(b);b.aoColumns[a]._select_selected=!0;a=(new g.Api(b)).column(a);f(a.header()).addClass(b._select.className);f(a.footer()).addClass(b._select.className);
|
||||||
|
a.nodes().to$().addClass(b._select.className)});this.iterator("table",function(a,d){m(b,"select",["column",b[d]],!0)});return this});q("cells().select()","cell().select()",function(a){var b=this;if(!1===a)return this.deselect();this.iterator("cell",function(b,a,e){r(b);a=b.aoData[a];a._selected_cells===h&&(a._selected_cells=[]);a._selected_cells[e]=!0;a.anCells&&f(a.anCells[e]).addClass(b._select.className)});this.iterator("table",function(a,d){m(b,"select",["cell",b[d]],!0)});return this});q("rows().deselect()",
|
||||||
|
"row().deselect()",function(){var a=this;this.iterator("row",function(a,c){a.aoData[c]._select_selected=!1;f(a.aoData[c].nTr).removeClass(a._select.className)});this.iterator("table",function(b,c){m(a,"deselect",["row",a[c]],!0)});return this});q("columns().deselect()","column().deselect()",function(){var a=this;this.iterator("column",function(a,c){a.aoColumns[c]._select_selected=!1;var b=new g.Api(a),e=b.column(c);f(e.header()).removeClass(a._select.className);f(e.footer()).removeClass(a._select.className);
|
||||||
|
b.cells(null,c).indexes().each(function(b){var c=a.aoData[b.row],d=c._selected_cells;!c.anCells||d&&d[b.column]||f(c.anCells[b.column]).removeClass(a._select.className)})});this.iterator("table",function(b,c){m(a,"deselect",["column",a[c]],!0)});return this});q("cells().deselect()","cell().deselect()",function(){var a=this;this.iterator("cell",function(a,c,d){c=a.aoData[c];c._selected_cells[d]=!1;c.anCells&&!a.aoColumns[d]._select_selected&&f(c.anCells[d]).removeClass(a._select.className)});this.iterator("table",
|
||||||
|
function(b,c){m(a,"deselect",["cell",a[c]],!0)});return this});var y=0;f.extend(g.ext.buttons,{selected:{text:t("selected","Selected"),className:"buttons-selected",limitTo:["rows","columns","cells"],init:function(a,b,c){var d=this;c._eventNamespace=".select"+y++;a.on(x(c),function(){d.enable(E(a,c))});this.disable()},destroy:function(a,b,c){a.off(c._eventNamespace)}},selectedSingle:{text:t("selectedSingle","Selected single"),className:"buttons-selected-single",init:function(a,b,c){var d=this;c._eventNamespace=
|
||||||
|
".select"+y++;a.on(x(c),function(){var b=a.rows({selected:!0}).flatten().length+a.columns({selected:!0}).flatten().length+a.cells({selected:!0}).flatten().length;d.enable(1===b)});this.disable()},destroy:function(a,b,c){a.off(c._eventNamespace)}},selectAll:{text:t("selectAll","Select all"),className:"buttons-select-all",action:function(){this[this.select.items()+"s"]().select()}},selectNone:{text:t("selectNone","Deselect all"),className:"buttons-select-none",action:function(){r(this.settings()[0],
|
||||||
|
!0)},init:function(a,b,c){var d=this;c._eventNamespace=".select"+y++;a.on(x(c),function(){var b=a.rows({selected:!0}).flatten().length+a.columns({selected:!0}).flatten().length+a.cells({selected:!0}).flatten().length;d.enable(0<b)});this.disable()},destroy:function(a,b,c){a.off(c._eventNamespace)}}});f.each(["Row","Column","Cell"],function(a,b){var c=b.toLowerCase();g.ext.buttons["select"+b+"s"]={text:t("select"+b+"s","Select "+c+"s"),className:"buttons-select-"+c+"s",action:function(){this.select.items(c)},
|
||||||
|
init:function(a){var b=this;a.on("selectItems.dt.DT",function(a,d,e){b.active(e===c)})}}});f(p).on("preInit.dt.dtSelect",function(a,b){"dt"===a.namespace&&g.select.init(new g.Api(b))});return g.select});
|
180
static/vendor/datatables/jquery.dataTables.min.js
vendored
Normal file
180
static/vendor/datatables/jquery.dataTables.min.js
vendored
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
/*!
|
||||||
|
Copyright 2008-2019 SpryMedia Ltd.
|
||||||
|
|
||||||
|
This source file is free software, available under the following license:
|
||||||
|
MIT license - http://datatables.net/license
|
||||||
|
|
||||||
|
This source file is distributed in the hope that it will be useful, but
|
||||||
|
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
|
||||||
|
|
||||||
|
For details please refer to: http://www.datatables.net
|
||||||
|
DataTables 1.10.20
|
||||||
|
©2008-2019 SpryMedia Ltd - datatables.net/license
|
||||||
|
*/
|
||||||
|
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.findInternal=function(f,z,y){f instanceof String&&(f=String(f));for(var p=f.length,H=0;H<p;H++){var L=f[H];if(z.call(y,L,H,f))return{i:H,v:L}}return{i:-1,v:void 0}};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.SIMPLE_FROUND_POLYFILL=!1;
|
||||||
|
$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(f,z,y){f!=Array.prototype&&f!=Object.prototype&&(f[z]=y.value)};$jscomp.getGlobal=function(f){return"undefined"!=typeof window&&window===f?f:"undefined"!=typeof global&&null!=global?global:f};$jscomp.global=$jscomp.getGlobal(this);
|
||||||
|
$jscomp.polyfill=function(f,z,y,p){if(z){y=$jscomp.global;f=f.split(".");for(p=0;p<f.length-1;p++){var H=f[p];H in y||(y[H]={});y=y[H]}f=f[f.length-1];p=y[f];z=z(p);z!=p&&null!=z&&$jscomp.defineProperty(y,f,{configurable:!0,writable:!0,value:z})}};$jscomp.polyfill("Array.prototype.find",function(f){return f?f:function(f,y){return $jscomp.findInternal(this,f,y).v}},"es6","es3");
|
||||||
|
(function(f){"function"===typeof define&&define.amd?define(["jquery"],function(z){return f(z,window,document)}):"object"===typeof exports?module.exports=function(z,y){z||(z=window);y||(y="undefined"!==typeof window?require("jquery"):require("jquery")(z));return f(y,z,z.document)}:f(jQuery,window,document)})(function(f,z,y,p){function H(a){var b,c,d={};f.each(a,function(e,h){(b=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(b[1]+" ")&&(c=e.replace(b[0],b[2].toLowerCase()),
|
||||||
|
d[c]=e,"o"===b[1]&&H(a[e]))});a._hungarianMap=d}function L(a,b,c){a._hungarianMap||H(a);var d;f.each(b,function(e,h){d=a._hungarianMap[e];d===p||!c&&b[d]!==p||("o"===d.charAt(0)?(b[d]||(b[d]={}),f.extend(!0,b[d],b[e]),L(a[d],b[d],c)):b[d]=b[e])})}function Ga(a){var b=q.defaults.oLanguage,c=b.sDecimal;c&&Ha(c);if(a){var d=a.sZeroRecords;!a.sEmptyTable&&d&&"No data available in table"===b.sEmptyTable&&M(a,a,"sZeroRecords","sEmptyTable");!a.sLoadingRecords&&d&&"Loading..."===b.sLoadingRecords&&M(a,a,
|
||||||
|
"sZeroRecords","sLoadingRecords");a.sInfoThousands&&(a.sThousands=a.sInfoThousands);(a=a.sDecimal)&&c!==a&&Ha(a)}}function jb(a){F(a,"ordering","bSort");F(a,"orderMulti","bSortMulti");F(a,"orderClasses","bSortClasses");F(a,"orderCellsTop","bSortCellsTop");F(a,"order","aaSorting");F(a,"orderFixed","aaSortingFixed");F(a,"paging","bPaginate");F(a,"pagingType","sPaginationType");F(a,"pageLength","iDisplayLength");F(a,"searching","bFilter");"boolean"===typeof a.sScrollX&&(a.sScrollX=a.sScrollX?"100%":
|
||||||
|
"");"boolean"===typeof a.scrollX&&(a.scrollX=a.scrollX?"100%":"");if(a=a.aoSearchCols)for(var b=0,c=a.length;b<c;b++)a[b]&&L(q.models.oSearch,a[b])}function kb(a){F(a,"orderable","bSortable");F(a,"orderData","aDataSort");F(a,"orderSequence","asSorting");F(a,"orderDataType","sortDataType");var b=a.aDataSort;"number"!==typeof b||f.isArray(b)||(a.aDataSort=[b])}function lb(a){if(!q.__browser){var b={};q.__browser=b;var c=f("<div/>").css({position:"fixed",top:0,left:-1*f(z).scrollLeft(),height:1,width:1,
|
||||||
|
overflow:"hidden"}).append(f("<div/>").css({position:"absolute",top:1,left:1,width:100,overflow:"scroll"}).append(f("<div/>").css({width:"100%",height:10}))).appendTo("body"),d=c.children(),e=d.children();b.barWidth=d[0].offsetWidth-d[0].clientWidth;b.bScrollOversize=100===e[0].offsetWidth&&100!==d[0].clientWidth;b.bScrollbarLeft=1!==Math.round(e.offset().left);b.bBounding=c[0].getBoundingClientRect().width?!0:!1;c.remove()}f.extend(a.oBrowser,q.__browser);a.oScroll.iBarWidth=q.__browser.barWidth}
|
||||||
|
function mb(a,b,c,d,e,h){var g=!1;if(c!==p){var k=c;g=!0}for(;d!==e;)a.hasOwnProperty(d)&&(k=g?b(k,a[d],d,a):a[d],g=!0,d+=h);return k}function Ia(a,b){var c=q.defaults.column,d=a.aoColumns.length;c=f.extend({},q.models.oColumn,c,{nTh:b?b:y.createElement("th"),sTitle:c.sTitle?c.sTitle:b?b.innerHTML:"",aDataSort:c.aDataSort?c.aDataSort:[d],mData:c.mData?c.mData:d,idx:d});a.aoColumns.push(c);c=a.aoPreSearchCols;c[d]=f.extend({},q.models.oSearch,c[d]);ma(a,d,f(b).data())}function ma(a,b,c){b=a.aoColumns[b];
|
||||||
|
var d=a.oClasses,e=f(b.nTh);if(!b.sWidthOrig){b.sWidthOrig=e.attr("width")||null;var h=(e.attr("style")||"").match(/width:\s*(\d+[pxem%]+)/);h&&(b.sWidthOrig=h[1])}c!==p&&null!==c&&(kb(c),L(q.defaults.column,c,!0),c.mDataProp===p||c.mData||(c.mData=c.mDataProp),c.sType&&(b._sManualType=c.sType),c.className&&!c.sClass&&(c.sClass=c.className),c.sClass&&e.addClass(c.sClass),f.extend(b,c),M(b,c,"sWidth","sWidthOrig"),c.iDataSort!==p&&(b.aDataSort=[c.iDataSort]),M(b,c,"aDataSort"));var g=b.mData,k=U(g),
|
||||||
|
l=b.mRender?U(b.mRender):null;c=function(a){return"string"===typeof a&&-1!==a.indexOf("@")};b._bAttrSrc=f.isPlainObject(g)&&(c(g.sort)||c(g.type)||c(g.filter));b._setter=null;b.fnGetData=function(a,b,c){var d=k(a,b,p,c);return l&&b?l(d,b,a,c):d};b.fnSetData=function(a,b,c){return Q(g)(a,b,c)};"number"!==typeof g&&(a._rowReadObject=!0);a.oFeatures.bSort||(b.bSortable=!1,e.addClass(d.sSortableNone));a=-1!==f.inArray("asc",b.asSorting);c=-1!==f.inArray("desc",b.asSorting);b.bSortable&&(a||c)?a&&!c?(b.sSortingClass=
|
||||||
|
d.sSortableAsc,b.sSortingClassJUI=d.sSortJUIAscAllowed):!a&&c?(b.sSortingClass=d.sSortableDesc,b.sSortingClassJUI=d.sSortJUIDescAllowed):(b.sSortingClass=d.sSortable,b.sSortingClassJUI=d.sSortJUI):(b.sSortingClass=d.sSortableNone,b.sSortingClassJUI="")}function aa(a){if(!1!==a.oFeatures.bAutoWidth){var b=a.aoColumns;Ja(a);for(var c=0,d=b.length;c<d;c++)b[c].nTh.style.width=b[c].sWidth}b=a.oScroll;""===b.sY&&""===b.sX||na(a);A(a,null,"column-sizing",[a])}function ba(a,b){a=oa(a,"bVisible");return"number"===
|
||||||
|
typeof a[b]?a[b]:null}function ca(a,b){a=oa(a,"bVisible");b=f.inArray(b,a);return-1!==b?b:null}function W(a){var b=0;f.each(a.aoColumns,function(a,d){d.bVisible&&"none"!==f(d.nTh).css("display")&&b++});return b}function oa(a,b){var c=[];f.map(a.aoColumns,function(a,e){a[b]&&c.push(e)});return c}function Ka(a){var b=a.aoColumns,c=a.aoData,d=q.ext.type.detect,e,h,g;var k=0;for(e=b.length;k<e;k++){var f=b[k];var n=[];if(!f.sType&&f._sManualType)f.sType=f._sManualType;else if(!f.sType){var m=0;for(h=
|
||||||
|
d.length;m<h;m++){var w=0;for(g=c.length;w<g;w++){n[w]===p&&(n[w]=I(a,w,k,"type"));var u=d[m](n[w],a);if(!u&&m!==d.length-1)break;if("html"===u)break}if(u){f.sType=u;break}}f.sType||(f.sType="string")}}}function nb(a,b,c,d){var e,h,g,k=a.aoColumns;if(b)for(e=b.length-1;0<=e;e--){var l=b[e];var n=l.targets!==p?l.targets:l.aTargets;f.isArray(n)||(n=[n]);var m=0;for(h=n.length;m<h;m++)if("number"===typeof n[m]&&0<=n[m]){for(;k.length<=n[m];)Ia(a);d(n[m],l)}else if("number"===typeof n[m]&&0>n[m])d(k.length+
|
||||||
|
n[m],l);else if("string"===typeof n[m]){var w=0;for(g=k.length;w<g;w++)("_all"==n[m]||f(k[w].nTh).hasClass(n[m]))&&d(w,l)}}if(c)for(e=0,a=c.length;e<a;e++)d(e,c[e])}function R(a,b,c,d){var e=a.aoData.length,h=f.extend(!0,{},q.models.oRow,{src:c?"dom":"data",idx:e});h._aData=b;a.aoData.push(h);for(var g=a.aoColumns,k=0,l=g.length;k<l;k++)g[k].sType=null;a.aiDisplayMaster.push(e);b=a.rowIdFn(b);b!==p&&(a.aIds[b]=h);!c&&a.oFeatures.bDeferRender||La(a,e,c,d);return e}function pa(a,b){var c;b instanceof
|
||||||
|
f||(b=f(b));return b.map(function(b,e){c=Ma(a,e);return R(a,c.data,e,c.cells)})}function I(a,b,c,d){var e=a.iDraw,h=a.aoColumns[c],g=a.aoData[b]._aData,k=h.sDefaultContent,f=h.fnGetData(g,d,{settings:a,row:b,col:c});if(f===p)return a.iDrawError!=e&&null===k&&(O(a,0,"Requested unknown parameter "+("function"==typeof h.mData?"{function}":"'"+h.mData+"'")+" for row "+b+", column "+c,4),a.iDrawError=e),k;if((f===g||null===f)&&null!==k&&d!==p)f=k;else if("function"===typeof f)return f.call(g);return null===
|
||||||
|
f&&"display"==d?"":f}function ob(a,b,c,d){a.aoColumns[c].fnSetData(a.aoData[b]._aData,d,{settings:a,row:b,col:c})}function Na(a){return f.map(a.match(/(\\.|[^\.])+/g)||[""],function(a){return a.replace(/\\\./g,".")})}function U(a){if(f.isPlainObject(a)){var b={};f.each(a,function(a,c){c&&(b[a]=U(c))});return function(a,c,h,g){var d=b[c]||b._;return d!==p?d(a,c,h,g):a}}if(null===a)return function(a){return a};if("function"===typeof a)return function(b,c,h,g){return a(b,c,h,g)};if("string"!==typeof a||
|
||||||
|
-1===a.indexOf(".")&&-1===a.indexOf("[")&&-1===a.indexOf("("))return function(b,c){return b[a]};var c=function(a,b,h){if(""!==h){var d=Na(h);for(var e=0,l=d.length;e<l;e++){h=d[e].match(da);var n=d[e].match(X);if(h){d[e]=d[e].replace(da,"");""!==d[e]&&(a=a[d[e]]);n=[];d.splice(0,e+1);d=d.join(".");if(f.isArray(a))for(e=0,l=a.length;e<l;e++)n.push(c(a[e],b,d));a=h[0].substring(1,h[0].length-1);a=""===a?n:n.join(a);break}else if(n){d[e]=d[e].replace(X,"");a=a[d[e]]();continue}if(null===a||a[d[e]]===
|
||||||
|
p)return p;a=a[d[e]]}}return a};return function(b,e){return c(b,e,a)}}function Q(a){if(f.isPlainObject(a))return Q(a._);if(null===a)return function(){};if("function"===typeof a)return function(b,d,e){a(b,"set",d,e)};if("string"!==typeof a||-1===a.indexOf(".")&&-1===a.indexOf("[")&&-1===a.indexOf("("))return function(b,d){b[a]=d};var b=function(a,d,e){e=Na(e);var c=e[e.length-1];for(var g,k,l=0,n=e.length-1;l<n;l++){g=e[l].match(da);k=e[l].match(X);if(g){e[l]=e[l].replace(da,"");a[e[l]]=[];c=e.slice();
|
||||||
|
c.splice(0,l+1);g=c.join(".");if(f.isArray(d))for(k=0,n=d.length;k<n;k++)c={},b(c,d[k],g),a[e[l]].push(c);else a[e[l]]=d;return}k&&(e[l]=e[l].replace(X,""),a=a[e[l]](d));if(null===a[e[l]]||a[e[l]]===p)a[e[l]]={};a=a[e[l]]}if(c.match(X))a[c.replace(X,"")](d);else a[c.replace(da,"")]=d};return function(c,d){return b(c,d,a)}}function Oa(a){return J(a.aoData,"_aData")}function qa(a){a.aoData.length=0;a.aiDisplayMaster.length=0;a.aiDisplay.length=0;a.aIds={}}function ra(a,b,c){for(var d=-1,e=0,h=a.length;e<
|
||||||
|
h;e++)a[e]==b?d=e:a[e]>b&&a[e]--; -1!=d&&c===p&&a.splice(d,1)}function ea(a,b,c,d){var e=a.aoData[b],h,g=function(c,d){for(;c.childNodes.length;)c.removeChild(c.firstChild);c.innerHTML=I(a,b,d,"display")};if("dom"!==c&&(c&&"auto"!==c||"dom"!==e.src)){var k=e.anCells;if(k)if(d!==p)g(k[d],d);else for(c=0,h=k.length;c<h;c++)g(k[c],c)}else e._aData=Ma(a,e,d,d===p?p:e._aData).data;e._aSortData=null;e._aFilterData=null;g=a.aoColumns;if(d!==p)g[d].sType=null;else{c=0;for(h=g.length;c<h;c++)g[c].sType=null;
|
||||||
|
Pa(a,e)}}function Ma(a,b,c,d){var e=[],h=b.firstChild,g,k=0,l,n=a.aoColumns,m=a._rowReadObject;d=d!==p?d:m?{}:[];var w=function(a,b){if("string"===typeof a){var c=a.indexOf("@");-1!==c&&(c=a.substring(c+1),Q(a)(d,b.getAttribute(c)))}},u=function(a){if(c===p||c===k)g=n[k],l=f.trim(a.innerHTML),g&&g._bAttrSrc?(Q(g.mData._)(d,l),w(g.mData.sort,a),w(g.mData.type,a),w(g.mData.filter,a)):m?(g._setter||(g._setter=Q(g.mData)),g._setter(d,l)):d[k]=l;k++};if(h)for(;h;){var q=h.nodeName.toUpperCase();if("TD"==
|
||||||
|
q||"TH"==q)u(h),e.push(h);h=h.nextSibling}else for(e=b.anCells,h=0,q=e.length;h<q;h++)u(e[h]);(b=b.firstChild?b:b.nTr)&&(b=b.getAttribute("id"))&&Q(a.rowId)(d,b);return{data:d,cells:e}}function La(a,b,c,d){var e=a.aoData[b],h=e._aData,g=[],k,l;if(null===e.nTr){var n=c||y.createElement("tr");e.nTr=n;e.anCells=g;n._DT_RowIndex=b;Pa(a,e);var m=0;for(k=a.aoColumns.length;m<k;m++){var w=a.aoColumns[m];var p=(l=c?!1:!0)?y.createElement(w.sCellType):d[m];p._DT_CellIndex={row:b,column:m};g.push(p);if(l||
|
||||||
|
!(c&&!w.mRender&&w.mData===m||f.isPlainObject(w.mData)&&w.mData._===m+".display"))p.innerHTML=I(a,b,m,"display");w.sClass&&(p.className+=" "+w.sClass);w.bVisible&&!c?n.appendChild(p):!w.bVisible&&c&&p.parentNode.removeChild(p);w.fnCreatedCell&&w.fnCreatedCell.call(a.oInstance,p,I(a,b,m),h,b,m)}A(a,"aoRowCreatedCallback",null,[n,h,b,g])}e.nTr.setAttribute("role","row")}function Pa(a,b){var c=b.nTr,d=b._aData;if(c){if(a=a.rowIdFn(d))c.id=a;d.DT_RowClass&&(a=d.DT_RowClass.split(" "),b.__rowc=b.__rowc?
|
||||||
|
ta(b.__rowc.concat(a)):a,f(c).removeClass(b.__rowc.join(" ")).addClass(d.DT_RowClass));d.DT_RowAttr&&f(c).attr(d.DT_RowAttr);d.DT_RowData&&f(c).data(d.DT_RowData)}}function pb(a){var b,c,d=a.nTHead,e=a.nTFoot,h=0===f("th, td",d).length,g=a.oClasses,k=a.aoColumns;h&&(c=f("<tr/>").appendTo(d));var l=0;for(b=k.length;l<b;l++){var n=k[l];var m=f(n.nTh).addClass(n.sClass);h&&m.appendTo(c);a.oFeatures.bSort&&(m.addClass(n.sSortingClass),!1!==n.bSortable&&(m.attr("tabindex",a.iTabIndex).attr("aria-controls",
|
||||||
|
a.sTableId),Qa(a,n.nTh,l)));n.sTitle!=m[0].innerHTML&&m.html(n.sTitle);Ra(a,"header")(a,m,n,g)}h&&fa(a.aoHeader,d);f(d).find(">tr").attr("role","row");f(d).find(">tr>th, >tr>td").addClass(g.sHeaderTH);f(e).find(">tr>th, >tr>td").addClass(g.sFooterTH);if(null!==e)for(a=a.aoFooter[0],l=0,b=a.length;l<b;l++)n=k[l],n.nTf=a[l].cell,n.sClass&&f(n.nTf).addClass(n.sClass)}function ha(a,b,c){var d,e,h=[],g=[],k=a.aoColumns.length;if(b){c===p&&(c=!1);var l=0;for(d=b.length;l<d;l++){h[l]=b[l].slice();h[l].nTr=
|
||||||
|
b[l].nTr;for(e=k-1;0<=e;e--)a.aoColumns[e].bVisible||c||h[l].splice(e,1);g.push([])}l=0;for(d=h.length;l<d;l++){if(a=h[l].nTr)for(;e=a.firstChild;)a.removeChild(e);e=0;for(b=h[l].length;e<b;e++){var n=k=1;if(g[l][e]===p){a.appendChild(h[l][e].cell);for(g[l][e]=1;h[l+k]!==p&&h[l][e].cell==h[l+k][e].cell;)g[l+k][e]=1,k++;for(;h[l][e+n]!==p&&h[l][e].cell==h[l][e+n].cell;){for(c=0;c<k;c++)g[l+c][e+n]=1;n++}f(h[l][e].cell).attr("rowspan",k).attr("colspan",n)}}}}}function S(a){var b=A(a,"aoPreDrawCallback",
|
||||||
|
"preDraw",[a]);if(-1!==f.inArray(!1,b))K(a,!1);else{b=[];var c=0,d=a.asStripeClasses,e=d.length,h=a.oLanguage,g=a.iInitDisplayStart,k="ssp"==D(a),l=a.aiDisplay;a.bDrawing=!0;g!==p&&-1!==g&&(a._iDisplayStart=k?g:g>=a.fnRecordsDisplay()?0:g,a.iInitDisplayStart=-1);g=a._iDisplayStart;var n=a.fnDisplayEnd();if(a.bDeferLoading)a.bDeferLoading=!1,a.iDraw++,K(a,!1);else if(!k)a.iDraw++;else if(!a.bDestroying&&!qb(a))return;if(0!==l.length)for(h=k?a.aoData.length:n,k=k?0:g;k<h;k++){var m=l[k],w=a.aoData[m];
|
||||||
|
null===w.nTr&&La(a,m);var u=w.nTr;if(0!==e){var q=d[c%e];w._sRowStripe!=q&&(f(u).removeClass(w._sRowStripe).addClass(q),w._sRowStripe=q)}A(a,"aoRowCallback",null,[u,w._aData,c,k,m]);b.push(u);c++}else c=h.sZeroRecords,1==a.iDraw&&"ajax"==D(a)?c=h.sLoadingRecords:h.sEmptyTable&&0===a.fnRecordsTotal()&&(c=h.sEmptyTable),b[0]=f("<tr/>",{"class":e?d[0]:""}).append(f("<td />",{valign:"top",colSpan:W(a),"class":a.oClasses.sRowEmpty}).html(c))[0];A(a,"aoHeaderCallback","header",[f(a.nTHead).children("tr")[0],
|
||||||
|
Oa(a),g,n,l]);A(a,"aoFooterCallback","footer",[f(a.nTFoot).children("tr")[0],Oa(a),g,n,l]);d=f(a.nTBody);d.children().detach();d.append(f(b));A(a,"aoDrawCallback","draw",[a]);a.bSorted=!1;a.bFiltered=!1;a.bDrawing=!1}}function V(a,b){var c=a.oFeatures,d=c.bFilter;c.bSort&&rb(a);d?ia(a,a.oPreviousSearch):a.aiDisplay=a.aiDisplayMaster.slice();!0!==b&&(a._iDisplayStart=0);a._drawHold=b;S(a);a._drawHold=!1}function sb(a){var b=a.oClasses,c=f(a.nTable);c=f("<div/>").insertBefore(c);var d=a.oFeatures,e=
|
||||||
|
f("<div/>",{id:a.sTableId+"_wrapper","class":b.sWrapper+(a.nTFoot?"":" "+b.sNoFooter)});a.nHolding=c[0];a.nTableWrapper=e[0];a.nTableReinsertBefore=a.nTable.nextSibling;for(var h=a.sDom.split(""),g,k,l,n,m,p,u=0;u<h.length;u++){g=null;k=h[u];if("<"==k){l=f("<div/>")[0];n=h[u+1];if("'"==n||'"'==n){m="";for(p=2;h[u+p]!=n;)m+=h[u+p],p++;"H"==m?m=b.sJUIHeader:"F"==m&&(m=b.sJUIFooter);-1!=m.indexOf(".")?(n=m.split("."),l.id=n[0].substr(1,n[0].length-1),l.className=n[1]):"#"==m.charAt(0)?l.id=m.substr(1,
|
||||||
|
m.length-1):l.className=m;u+=p}e.append(l);e=f(l)}else if(">"==k)e=e.parent();else if("l"==k&&d.bPaginate&&d.bLengthChange)g=tb(a);else if("f"==k&&d.bFilter)g=ub(a);else if("r"==k&&d.bProcessing)g=vb(a);else if("t"==k)g=wb(a);else if("i"==k&&d.bInfo)g=xb(a);else if("p"==k&&d.bPaginate)g=yb(a);else if(0!==q.ext.feature.length)for(l=q.ext.feature,p=0,n=l.length;p<n;p++)if(k==l[p].cFeature){g=l[p].fnInit(a);break}g&&(l=a.aanFeatures,l[k]||(l[k]=[]),l[k].push(g),e.append(g))}c.replaceWith(e);a.nHolding=
|
||||||
|
null}function fa(a,b){b=f(b).children("tr");var c,d,e;a.splice(0,a.length);var h=0;for(e=b.length;h<e;h++)a.push([]);h=0;for(e=b.length;h<e;h++){var g=b[h];for(c=g.firstChild;c;){if("TD"==c.nodeName.toUpperCase()||"TH"==c.nodeName.toUpperCase()){var k=1*c.getAttribute("colspan");var l=1*c.getAttribute("rowspan");k=k&&0!==k&&1!==k?k:1;l=l&&0!==l&&1!==l?l:1;var n=0;for(d=a[h];d[n];)n++;var m=n;var p=1===k?!0:!1;for(d=0;d<k;d++)for(n=0;n<l;n++)a[h+n][m+d]={cell:c,unique:p},a[h+n].nTr=g}c=c.nextSibling}}}
|
||||||
|
function ua(a,b,c){var d=[];c||(c=a.aoHeader,b&&(c=[],fa(c,b)));b=0;for(var e=c.length;b<e;b++)for(var h=0,g=c[b].length;h<g;h++)!c[b][h].unique||d[h]&&a.bSortCellsTop||(d[h]=c[b][h].cell);return d}function va(a,b,c){A(a,"aoServerParams","serverParams",[b]);if(b&&f.isArray(b)){var d={},e=/(.*?)\[\]$/;f.each(b,function(a,b){(a=b.name.match(e))?(a=a[0],d[a]||(d[a]=[]),d[a].push(b.value)):d[b.name]=b.value});b=d}var h=a.ajax,g=a.oInstance,k=function(b){A(a,null,"xhr",[a,b,a.jqXHR]);c(b)};if(f.isPlainObject(h)&&
|
||||||
|
h.data){var l=h.data;var n="function"===typeof l?l(b,a):l;b="function"===typeof l&&n?n:f.extend(!0,b,n);delete h.data}n={data:b,success:function(b){var c=b.error||b.sError;c&&O(a,0,c);a.json=b;k(b)},dataType:"json",cache:!1,type:a.sServerMethod,error:function(b,c,d){d=A(a,null,"xhr",[a,null,a.jqXHR]);-1===f.inArray(!0,d)&&("parsererror"==c?O(a,0,"Invalid JSON response",1):4===b.readyState&&O(a,0,"Ajax error",7));K(a,!1)}};a.oAjaxData=b;A(a,null,"preXhr",[a,b]);a.fnServerData?a.fnServerData.call(g,
|
||||||
|
a.sAjaxSource,f.map(b,function(a,b){return{name:b,value:a}}),k,a):a.sAjaxSource||"string"===typeof h?a.jqXHR=f.ajax(f.extend(n,{url:h||a.sAjaxSource})):"function"===typeof h?a.jqXHR=h.call(g,b,k,a):(a.jqXHR=f.ajax(f.extend(n,h)),h.data=l)}function qb(a){return a.bAjaxDataGet?(a.iDraw++,K(a,!0),va(a,zb(a),function(b){Ab(a,b)}),!1):!0}function zb(a){var b=a.aoColumns,c=b.length,d=a.oFeatures,e=a.oPreviousSearch,h=a.aoPreSearchCols,g=[],k=Y(a);var l=a._iDisplayStart;var n=!1!==d.bPaginate?a._iDisplayLength:
|
||||||
|
-1;var m=function(a,b){g.push({name:a,value:b})};m("sEcho",a.iDraw);m("iColumns",c);m("sColumns",J(b,"sName").join(","));m("iDisplayStart",l);m("iDisplayLength",n);var p={draw:a.iDraw,columns:[],order:[],start:l,length:n,search:{value:e.sSearch,regex:e.bRegex}};for(l=0;l<c;l++){var u=b[l];var sa=h[l];n="function"==typeof u.mData?"function":u.mData;p.columns.push({data:n,name:u.sName,searchable:u.bSearchable,orderable:u.bSortable,search:{value:sa.sSearch,regex:sa.bRegex}});m("mDataProp_"+l,n);d.bFilter&&
|
||||||
|
(m("sSearch_"+l,sa.sSearch),m("bRegex_"+l,sa.bRegex),m("bSearchable_"+l,u.bSearchable));d.bSort&&m("bSortable_"+l,u.bSortable)}d.bFilter&&(m("sSearch",e.sSearch),m("bRegex",e.bRegex));d.bSort&&(f.each(k,function(a,b){p.order.push({column:b.col,dir:b.dir});m("iSortCol_"+a,b.col);m("sSortDir_"+a,b.dir)}),m("iSortingCols",k.length));b=q.ext.legacy.ajax;return null===b?a.sAjaxSource?g:p:b?g:p}function Ab(a,b){var c=function(a,c){return b[a]!==p?b[a]:b[c]},d=wa(a,b),e=c("sEcho","draw"),h=c("iTotalRecords",
|
||||||
|
"recordsTotal");c=c("iTotalDisplayRecords","recordsFiltered");if(e){if(1*e<a.iDraw)return;a.iDraw=1*e}qa(a);a._iRecordsTotal=parseInt(h,10);a._iRecordsDisplay=parseInt(c,10);e=0;for(h=d.length;e<h;e++)R(a,d[e]);a.aiDisplay=a.aiDisplayMaster.slice();a.bAjaxDataGet=!1;S(a);a._bInitComplete||xa(a,b);a.bAjaxDataGet=!0;K(a,!1)}function wa(a,b){a=f.isPlainObject(a.ajax)&&a.ajax.dataSrc!==p?a.ajax.dataSrc:a.sAjaxDataProp;return"data"===a?b.aaData||b[a]:""!==a?U(a)(b):b}function ub(a){var b=a.oClasses,c=
|
||||||
|
a.sTableId,d=a.oLanguage,e=a.oPreviousSearch,h=a.aanFeatures,g='<input type="search" class="'+b.sFilterInput+'"/>',k=d.sSearch;k=k.match(/_INPUT_/)?k.replace("_INPUT_",g):k+g;b=f("<div/>",{id:h.f?null:c+"_filter","class":b.sFilter}).append(f("<label/>").append(k));h=function(){var b=this.value?this.value:"";b!=e.sSearch&&(ia(a,{sSearch:b,bRegex:e.bRegex,bSmart:e.bSmart,bCaseInsensitive:e.bCaseInsensitive}),a._iDisplayStart=0,S(a))};g=null!==a.searchDelay?a.searchDelay:"ssp"===D(a)?400:0;var l=f("input",
|
||||||
|
b).val(e.sSearch).attr("placeholder",d.sSearchPlaceholder).on("keyup.DT search.DT input.DT paste.DT cut.DT",g?Sa(h,g):h).on("keypress.DT",function(a){if(13==a.keyCode)return!1}).attr("aria-controls",c);f(a.nTable).on("search.dt.DT",function(b,c){if(a===c)try{l[0]!==y.activeElement&&l.val(e.sSearch)}catch(w){}});return b[0]}function ia(a,b,c){var d=a.oPreviousSearch,e=a.aoPreSearchCols,h=function(a){d.sSearch=a.sSearch;d.bRegex=a.bRegex;d.bSmart=a.bSmart;d.bCaseInsensitive=a.bCaseInsensitive},g=function(a){return a.bEscapeRegex!==
|
||||||
|
p?!a.bEscapeRegex:a.bRegex};Ka(a);if("ssp"!=D(a)){Bb(a,b.sSearch,c,g(b),b.bSmart,b.bCaseInsensitive);h(b);for(b=0;b<e.length;b++)Cb(a,e[b].sSearch,b,g(e[b]),e[b].bSmart,e[b].bCaseInsensitive);Db(a)}else h(b);a.bFiltered=!0;A(a,null,"search",[a])}function Db(a){for(var b=q.ext.search,c=a.aiDisplay,d,e,h=0,g=b.length;h<g;h++){for(var k=[],l=0,n=c.length;l<n;l++)e=c[l],d=a.aoData[e],b[h](a,d._aFilterData,e,d._aData,l)&&k.push(e);c.length=0;f.merge(c,k)}}function Cb(a,b,c,d,e,h){if(""!==b){var g=[],k=
|
||||||
|
a.aiDisplay;d=Ta(b,d,e,h);for(e=0;e<k.length;e++)b=a.aoData[k[e]]._aFilterData[c],d.test(b)&&g.push(k[e]);a.aiDisplay=g}}function Bb(a,b,c,d,e,h){e=Ta(b,d,e,h);var g=a.oPreviousSearch.sSearch,k=a.aiDisplayMaster;h=[];0!==q.ext.search.length&&(c=!0);var f=Eb(a);if(0>=b.length)a.aiDisplay=k.slice();else{if(f||c||d||g.length>b.length||0!==b.indexOf(g)||a.bSorted)a.aiDisplay=k.slice();b=a.aiDisplay;for(c=0;c<b.length;c++)e.test(a.aoData[b[c]]._sFilterRow)&&h.push(b[c]);a.aiDisplay=h}}function Ta(a,b,
|
||||||
|
c,d){a=b?a:Ua(a);c&&(a="^(?=.*?"+f.map(a.match(/"[^"]+"|[^ ]+/g)||[""],function(a){if('"'===a.charAt(0)){var b=a.match(/^"(.*)"$/);a=b?b[1]:a}return a.replace('"',"")}).join(")(?=.*?")+").*$");return new RegExp(a,d?"i":"")}function Eb(a){var b=a.aoColumns,c,d,e=q.ext.type.search;var h=!1;var g=0;for(c=a.aoData.length;g<c;g++){var k=a.aoData[g];if(!k._aFilterData){var f=[];var n=0;for(d=b.length;n<d;n++){h=b[n];if(h.bSearchable){var m=I(a,g,n,"filter");e[h.sType]&&(m=e[h.sType](m));null===m&&(m="");
|
||||||
|
"string"!==typeof m&&m.toString&&(m=m.toString())}else m="";m.indexOf&&-1!==m.indexOf("&")&&(ya.innerHTML=m,m=$b?ya.textContent:ya.innerText);m.replace&&(m=m.replace(/[\r\n\u2028]/g,""));f.push(m)}k._aFilterData=f;k._sFilterRow=f.join(" ");h=!0}}return h}function Fb(a){return{search:a.sSearch,smart:a.bSmart,regex:a.bRegex,caseInsensitive:a.bCaseInsensitive}}function Gb(a){return{sSearch:a.search,bSmart:a.smart,bRegex:a.regex,bCaseInsensitive:a.caseInsensitive}}function xb(a){var b=a.sTableId,c=a.aanFeatures.i,
|
||||||
|
d=f("<div/>",{"class":a.oClasses.sInfo,id:c?null:b+"_info"});c||(a.aoDrawCallback.push({fn:Hb,sName:"information"}),d.attr("role","status").attr("aria-live","polite"),f(a.nTable).attr("aria-describedby",b+"_info"));return d[0]}function Hb(a){var b=a.aanFeatures.i;if(0!==b.length){var c=a.oLanguage,d=a._iDisplayStart+1,e=a.fnDisplayEnd(),h=a.fnRecordsTotal(),g=a.fnRecordsDisplay(),k=g?c.sInfo:c.sInfoEmpty;g!==h&&(k+=" "+c.sInfoFiltered);k+=c.sInfoPostFix;k=Ib(a,k);c=c.fnInfoCallback;null!==c&&(k=c.call(a.oInstance,
|
||||||
|
a,d,e,h,g,k));f(b).html(k)}}function Ib(a,b){var c=a.fnFormatNumber,d=a._iDisplayStart+1,e=a._iDisplayLength,h=a.fnRecordsDisplay(),g=-1===e;return b.replace(/_START_/g,c.call(a,d)).replace(/_END_/g,c.call(a,a.fnDisplayEnd())).replace(/_MAX_/g,c.call(a,a.fnRecordsTotal())).replace(/_TOTAL_/g,c.call(a,h)).replace(/_PAGE_/g,c.call(a,g?1:Math.ceil(d/e))).replace(/_PAGES_/g,c.call(a,g?1:Math.ceil(h/e)))}function ja(a){var b=a.iInitDisplayStart,c=a.aoColumns;var d=a.oFeatures;var e=a.bDeferLoading;if(a.bInitialised){sb(a);
|
||||||
|
pb(a);ha(a,a.aoHeader);ha(a,a.aoFooter);K(a,!0);d.bAutoWidth&&Ja(a);var h=0;for(d=c.length;h<d;h++){var g=c[h];g.sWidth&&(g.nTh.style.width=B(g.sWidth))}A(a,null,"preInit",[a]);V(a);c=D(a);if("ssp"!=c||e)"ajax"==c?va(a,[],function(c){var d=wa(a,c);for(h=0;h<d.length;h++)R(a,d[h]);a.iInitDisplayStart=b;V(a);K(a,!1);xa(a,c)},a):(K(a,!1),xa(a))}else setTimeout(function(){ja(a)},200)}function xa(a,b){a._bInitComplete=!0;(b||a.oInit.aaData)&&aa(a);A(a,null,"plugin-init",[a,b]);A(a,"aoInitComplete","init",
|
||||||
|
[a,b])}function Va(a,b){b=parseInt(b,10);a._iDisplayLength=b;Wa(a);A(a,null,"length",[a,b])}function tb(a){var b=a.oClasses,c=a.sTableId,d=a.aLengthMenu,e=f.isArray(d[0]),h=e?d[0]:d;d=e?d[1]:d;e=f("<select/>",{name:c+"_length","aria-controls":c,"class":b.sLengthSelect});for(var g=0,k=h.length;g<k;g++)e[0][g]=new Option("number"===typeof d[g]?a.fnFormatNumber(d[g]):d[g],h[g]);var l=f("<div><label/></div>").addClass(b.sLength);a.aanFeatures.l||(l[0].id=c+"_length");l.children().append(a.oLanguage.sLengthMenu.replace("_MENU_",
|
||||||
|
e[0].outerHTML));f("select",l).val(a._iDisplayLength).on("change.DT",function(b){Va(a,f(this).val());S(a)});f(a.nTable).on("length.dt.DT",function(b,c,d){a===c&&f("select",l).val(d)});return l[0]}function yb(a){var b=a.sPaginationType,c=q.ext.pager[b],d="function"===typeof c,e=function(a){S(a)};b=f("<div/>").addClass(a.oClasses.sPaging+b)[0];var h=a.aanFeatures;d||c.fnInit(a,b,e);h.p||(b.id=a.sTableId+"_paginate",a.aoDrawCallback.push({fn:function(a){if(d){var b=a._iDisplayStart,g=a._iDisplayLength,
|
||||||
|
f=a.fnRecordsDisplay(),m=-1===g;b=m?0:Math.ceil(b/g);g=m?1:Math.ceil(f/g);f=c(b,g);var p;m=0;for(p=h.p.length;m<p;m++)Ra(a,"pageButton")(a,h.p[m],m,f,b,g)}else c.fnUpdate(a,e)},sName:"pagination"}));return b}function Xa(a,b,c){var d=a._iDisplayStart,e=a._iDisplayLength,h=a.fnRecordsDisplay();0===h||-1===e?d=0:"number"===typeof b?(d=b*e,d>h&&(d=0)):"first"==b?d=0:"previous"==b?(d=0<=e?d-e:0,0>d&&(d=0)):"next"==b?d+e<h&&(d+=e):"last"==b?d=Math.floor((h-1)/e)*e:O(a,0,"Unknown paging action: "+b,5);b=
|
||||||
|
a._iDisplayStart!==d;a._iDisplayStart=d;b&&(A(a,null,"page",[a]),c&&S(a));return b}function vb(a){return f("<div/>",{id:a.aanFeatures.r?null:a.sTableId+"_processing","class":a.oClasses.sProcessing}).html(a.oLanguage.sProcessing).insertBefore(a.nTable)[0]}function K(a,b){a.oFeatures.bProcessing&&f(a.aanFeatures.r).css("display",b?"block":"none");A(a,null,"processing",[a,b])}function wb(a){var b=f(a.nTable);b.attr("role","grid");var c=a.oScroll;if(""===c.sX&&""===c.sY)return a.nTable;var d=c.sX,e=c.sY,
|
||||||
|
h=a.oClasses,g=b.children("caption"),k=g.length?g[0]._captionSide:null,l=f(b[0].cloneNode(!1)),n=f(b[0].cloneNode(!1)),m=b.children("tfoot");m.length||(m=null);l=f("<div/>",{"class":h.sScrollWrapper}).append(f("<div/>",{"class":h.sScrollHead}).css({overflow:"hidden",position:"relative",border:0,width:d?d?B(d):null:"100%"}).append(f("<div/>",{"class":h.sScrollHeadInner}).css({"box-sizing":"content-box",width:c.sXInner||"100%"}).append(l.removeAttr("id").css("margin-left",0).append("top"===k?g:null).append(b.children("thead"))))).append(f("<div/>",
|
||||||
|
{"class":h.sScrollBody}).css({position:"relative",overflow:"auto",width:d?B(d):null}).append(b));m&&l.append(f("<div/>",{"class":h.sScrollFoot}).css({overflow:"hidden",border:0,width:d?d?B(d):null:"100%"}).append(f("<div/>",{"class":h.sScrollFootInner}).append(n.removeAttr("id").css("margin-left",0).append("bottom"===k?g:null).append(b.children("tfoot")))));b=l.children();var p=b[0];h=b[1];var u=m?b[2]:null;if(d)f(h).on("scroll.DT",function(a){a=this.scrollLeft;p.scrollLeft=a;m&&(u.scrollLeft=a)});
|
||||||
|
f(h).css(e&&c.bCollapse?"max-height":"height",e);a.nScrollHead=p;a.nScrollBody=h;a.nScrollFoot=u;a.aoDrawCallback.push({fn:na,sName:"scrolling"});return l[0]}function na(a){var b=a.oScroll,c=b.sX,d=b.sXInner,e=b.sY;b=b.iBarWidth;var h=f(a.nScrollHead),g=h[0].style,k=h.children("div"),l=k[0].style,n=k.children("table");k=a.nScrollBody;var m=f(k),w=k.style,u=f(a.nScrollFoot).children("div"),q=u.children("table"),t=f(a.nTHead),r=f(a.nTable),v=r[0],za=v.style,T=a.nTFoot?f(a.nTFoot):null,A=a.oBrowser,
|
||||||
|
x=A.bScrollOversize,ac=J(a.aoColumns,"nTh"),Ya=[],y=[],z=[],C=[],G,H=function(a){a=a.style;a.paddingTop="0";a.paddingBottom="0";a.borderTopWidth="0";a.borderBottomWidth="0";a.height=0};var D=k.scrollHeight>k.clientHeight;if(a.scrollBarVis!==D&&a.scrollBarVis!==p)a.scrollBarVis=D,aa(a);else{a.scrollBarVis=D;r.children("thead, tfoot").remove();if(T){var E=T.clone().prependTo(r);var F=T.find("tr");E=E.find("tr")}var I=t.clone().prependTo(r);t=t.find("tr");D=I.find("tr");I.find("th, td").removeAttr("tabindex");
|
||||||
|
c||(w.width="100%",h[0].style.width="100%");f.each(ua(a,I),function(b,c){G=ba(a,b);c.style.width=a.aoColumns[G].sWidth});T&&N(function(a){a.style.width=""},E);h=r.outerWidth();""===c?(za.width="100%",x&&(r.find("tbody").height()>k.offsetHeight||"scroll"==m.css("overflow-y"))&&(za.width=B(r.outerWidth()-b)),h=r.outerWidth()):""!==d&&(za.width=B(d),h=r.outerWidth());N(H,D);N(function(a){z.push(a.innerHTML);Ya.push(B(f(a).css("width")))},D);N(function(a,b){-1!==f.inArray(a,ac)&&(a.style.width=Ya[b])},
|
||||||
|
t);f(D).height(0);T&&(N(H,E),N(function(a){C.push(a.innerHTML);y.push(B(f(a).css("width")))},E),N(function(a,b){a.style.width=y[b]},F),f(E).height(0));N(function(a,b){a.innerHTML='<div class="dataTables_sizing">'+z[b]+"</div>";a.childNodes[0].style.height="0";a.childNodes[0].style.overflow="hidden";a.style.width=Ya[b]},D);T&&N(function(a,b){a.innerHTML='<div class="dataTables_sizing">'+C[b]+"</div>";a.childNodes[0].style.height="0";a.childNodes[0].style.overflow="hidden";a.style.width=y[b]},E);r.outerWidth()<
|
||||||
|
h?(F=k.scrollHeight>k.offsetHeight||"scroll"==m.css("overflow-y")?h+b:h,x&&(k.scrollHeight>k.offsetHeight||"scroll"==m.css("overflow-y"))&&(za.width=B(F-b)),""!==c&&""===d||O(a,1,"Possible column misalignment",6)):F="100%";w.width=B(F);g.width=B(F);T&&(a.nScrollFoot.style.width=B(F));!e&&x&&(w.height=B(v.offsetHeight+b));c=r.outerWidth();n[0].style.width=B(c);l.width=B(c);d=r.height()>k.clientHeight||"scroll"==m.css("overflow-y");e="padding"+(A.bScrollbarLeft?"Left":"Right");l[e]=d?b+"px":"0px";T&&
|
||||||
|
(q[0].style.width=B(c),u[0].style.width=B(c),u[0].style[e]=d?b+"px":"0px");r.children("colgroup").insertBefore(r.children("thead"));m.trigger("scroll");!a.bSorted&&!a.bFiltered||a._drawHold||(k.scrollTop=0)}}function N(a,b,c){for(var d=0,e=0,h=b.length,g,k;e<h;){g=b[e].firstChild;for(k=c?c[e].firstChild:null;g;)1===g.nodeType&&(c?a(g,k,d):a(g,d),d++),g=g.nextSibling,k=c?k.nextSibling:null;e++}}function Ja(a){var b=a.nTable,c=a.aoColumns,d=a.oScroll,e=d.sY,h=d.sX,g=d.sXInner,k=c.length,l=oa(a,"bVisible"),
|
||||||
|
n=f("th",a.nTHead),m=b.getAttribute("width"),p=b.parentNode,u=!1,q,t=a.oBrowser;d=t.bScrollOversize;(q=b.style.width)&&-1!==q.indexOf("%")&&(m=q);for(q=0;q<l.length;q++){var r=c[l[q]];null!==r.sWidth&&(r.sWidth=Jb(r.sWidthOrig,p),u=!0)}if(d||!u&&!h&&!e&&k==W(a)&&k==n.length)for(q=0;q<k;q++)l=ba(a,q),null!==l&&(c[l].sWidth=B(n.eq(q).width()));else{k=f(b).clone().css("visibility","hidden").removeAttr("id");k.find("tbody tr").remove();var v=f("<tr/>").appendTo(k.find("tbody"));k.find("thead, tfoot").remove();
|
||||||
|
k.append(f(a.nTHead).clone()).append(f(a.nTFoot).clone());k.find("tfoot th, tfoot td").css("width","");n=ua(a,k.find("thead")[0]);for(q=0;q<l.length;q++)r=c[l[q]],n[q].style.width=null!==r.sWidthOrig&&""!==r.sWidthOrig?B(r.sWidthOrig):"",r.sWidthOrig&&h&&f(n[q]).append(f("<div/>").css({width:r.sWidthOrig,margin:0,padding:0,border:0,height:1}));if(a.aoData.length)for(q=0;q<l.length;q++)u=l[q],r=c[u],f(Kb(a,u)).clone(!1).append(r.sContentPadding).appendTo(v);f("[name]",k).removeAttr("name");r=f("<div/>").css(h||
|
||||||
|
e?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(k).appendTo(p);h&&g?k.width(g):h?(k.css("width","auto"),k.removeAttr("width"),k.width()<p.clientWidth&&m&&k.width(p.clientWidth)):e?k.width(p.clientWidth):m&&k.width(m);for(q=e=0;q<l.length;q++)p=f(n[q]),g=p.outerWidth()-p.width(),p=t.bBounding?Math.ceil(n[q].getBoundingClientRect().width):p.outerWidth(),e+=p,c[l[q]].sWidth=B(p-g);b.style.width=B(e);r.remove()}m&&(b.style.width=B(m));!m&&!h||a._reszEvt||(b=function(){f(z).on("resize.DT-"+
|
||||||
|
a.sInstance,Sa(function(){aa(a)}))},d?setTimeout(b,1E3):b(),a._reszEvt=!0)}function Jb(a,b){if(!a)return 0;a=f("<div/>").css("width",B(a)).appendTo(b||y.body);b=a[0].offsetWidth;a.remove();return b}function Kb(a,b){var c=Lb(a,b);if(0>c)return null;var d=a.aoData[c];return d.nTr?d.anCells[b]:f("<td/>").html(I(a,c,b,"display"))[0]}function Lb(a,b){for(var c,d=-1,e=-1,h=0,g=a.aoData.length;h<g;h++)c=I(a,h,b,"display")+"",c=c.replace(bc,""),c=c.replace(/ /g," "),c.length>d&&(d=c.length,e=h);return e}
|
||||||
|
function B(a){return null===a?"0px":"number"==typeof a?0>a?"0px":a+"px":a.match(/\d$/)?a+"px":a}function Y(a){var b=[],c=a.aoColumns;var d=a.aaSortingFixed;var e=f.isPlainObject(d);var h=[];var g=function(a){a.length&&!f.isArray(a[0])?h.push(a):f.merge(h,a)};f.isArray(d)&&g(d);e&&d.pre&&g(d.pre);g(a.aaSorting);e&&d.post&&g(d.post);for(a=0;a<h.length;a++){var k=h[a][0];g=c[k].aDataSort;d=0;for(e=g.length;d<e;d++){var l=g[d];var n=c[l].sType||"string";h[a]._idx===p&&(h[a]._idx=f.inArray(h[a][1],c[l].asSorting));
|
||||||
|
b.push({src:k,col:l,dir:h[a][1],index:h[a]._idx,type:n,formatter:q.ext.type.order[n+"-pre"]})}}return b}function rb(a){var b,c=[],d=q.ext.type.order,e=a.aoData,h=0,g=a.aiDisplayMaster;Ka(a);var k=Y(a);var f=0;for(b=k.length;f<b;f++){var n=k[f];n.formatter&&h++;Mb(a,n.col)}if("ssp"!=D(a)&&0!==k.length){f=0;for(b=g.length;f<b;f++)c[g[f]]=f;h===k.length?g.sort(function(a,b){var d,h=k.length,g=e[a]._aSortData,f=e[b]._aSortData;for(d=0;d<h;d++){var l=k[d];var m=g[l.col];var n=f[l.col];m=m<n?-1:m>n?1:0;
|
||||||
|
if(0!==m)return"asc"===l.dir?m:-m}m=c[a];n=c[b];return m<n?-1:m>n?1:0}):g.sort(function(a,b){var h,g=k.length,f=e[a]._aSortData,l=e[b]._aSortData;for(h=0;h<g;h++){var m=k[h];var n=f[m.col];var p=l[m.col];m=d[m.type+"-"+m.dir]||d["string-"+m.dir];n=m(n,p);if(0!==n)return n}n=c[a];p=c[b];return n<p?-1:n>p?1:0})}a.bSorted=!0}function Nb(a){var b=a.aoColumns,c=Y(a);a=a.oLanguage.oAria;for(var d=0,e=b.length;d<e;d++){var h=b[d];var g=h.asSorting;var k=h.sTitle.replace(/<.*?>/g,"");var f=h.nTh;f.removeAttribute("aria-sort");
|
||||||
|
h.bSortable&&(0<c.length&&c[0].col==d?(f.setAttribute("aria-sort","asc"==c[0].dir?"ascending":"descending"),h=g[c[0].index+1]||g[0]):h=g[0],k+="asc"===h?a.sSortAscending:a.sSortDescending);f.setAttribute("aria-label",k)}}function Za(a,b,c,d){var e=a.aaSorting,h=a.aoColumns[b].asSorting,g=function(a,b){var c=a._idx;c===p&&(c=f.inArray(a[1],h));return c+1<h.length?c+1:b?null:0};"number"===typeof e[0]&&(e=a.aaSorting=[e]);c&&a.oFeatures.bSortMulti?(c=f.inArray(b,J(e,"0")),-1!==c?(b=g(e[c],!0),null===
|
||||||
|
b&&1===e.length&&(b=0),null===b?e.splice(c,1):(e[c][1]=h[b],e[c]._idx=b)):(e.push([b,h[0],0]),e[e.length-1]._idx=0)):e.length&&e[0][0]==b?(b=g(e[0]),e.length=1,e[0][1]=h[b],e[0]._idx=b):(e.length=0,e.push([b,h[0]]),e[0]._idx=0);V(a);"function"==typeof d&&d(a)}function Qa(a,b,c,d){var e=a.aoColumns[c];$a(b,{},function(b){!1!==e.bSortable&&(a.oFeatures.bProcessing?(K(a,!0),setTimeout(function(){Za(a,c,b.shiftKey,d);"ssp"!==D(a)&&K(a,!1)},0)):Za(a,c,b.shiftKey,d))})}function Aa(a){var b=a.aLastSort,
|
||||||
|
c=a.oClasses.sSortColumn,d=Y(a),e=a.oFeatures,h;if(e.bSort&&e.bSortClasses){e=0;for(h=b.length;e<h;e++){var g=b[e].src;f(J(a.aoData,"anCells",g)).removeClass(c+(2>e?e+1:3))}e=0;for(h=d.length;e<h;e++)g=d[e].src,f(J(a.aoData,"anCells",g)).addClass(c+(2>e?e+1:3))}a.aLastSort=d}function Mb(a,b){var c=a.aoColumns[b],d=q.ext.order[c.sSortDataType],e;d&&(e=d.call(a.oInstance,a,b,ca(a,b)));for(var h,g=q.ext.type.order[c.sType+"-pre"],k=0,f=a.aoData.length;k<f;k++)if(c=a.aoData[k],c._aSortData||(c._aSortData=
|
||||||
|
[]),!c._aSortData[b]||d)h=d?e[k]:I(a,k,b,"sort"),c._aSortData[b]=g?g(h):h}function Ba(a){if(a.oFeatures.bStateSave&&!a.bDestroying){var b={time:+new Date,start:a._iDisplayStart,length:a._iDisplayLength,order:f.extend(!0,[],a.aaSorting),search:Fb(a.oPreviousSearch),columns:f.map(a.aoColumns,function(b,d){return{visible:b.bVisible,search:Fb(a.aoPreSearchCols[d])}})};A(a,"aoStateSaveParams","stateSaveParams",[a,b]);a.oSavedState=b;a.fnStateSaveCallback.call(a.oInstance,a,b)}}function Ob(a,b,c){var d,
|
||||||
|
e,h=a.aoColumns;b=function(b){if(b&&b.time){var g=A(a,"aoStateLoadParams","stateLoadParams",[a,b]);if(-1===f.inArray(!1,g)&&(g=a.iStateDuration,!(0<g&&b.time<+new Date-1E3*g||b.columns&&h.length!==b.columns.length))){a.oLoadedState=f.extend(!0,{},b);b.start!==p&&(a._iDisplayStart=b.start,a.iInitDisplayStart=b.start);b.length!==p&&(a._iDisplayLength=b.length);b.order!==p&&(a.aaSorting=[],f.each(b.order,function(b,c){a.aaSorting.push(c[0]>=h.length?[0,c[1]]:c)}));b.search!==p&&f.extend(a.oPreviousSearch,
|
||||||
|
Gb(b.search));if(b.columns)for(d=0,e=b.columns.length;d<e;d++)g=b.columns[d],g.visible!==p&&(h[d].bVisible=g.visible),g.search!==p&&f.extend(a.aoPreSearchCols[d],Gb(g.search));A(a,"aoStateLoaded","stateLoaded",[a,b])}}c()};if(a.oFeatures.bStateSave){var g=a.fnStateLoadCallback.call(a.oInstance,a,b);g!==p&&b(g)}else c()}function Ca(a){var b=q.settings;a=f.inArray(a,J(b,"nTable"));return-1!==a?b[a]:null}function O(a,b,c,d){c="DataTables warning: "+(a?"table id="+a.sTableId+" - ":"")+c;d&&(c+=". For more information about this error, please see http://datatables.net/tn/"+
|
||||||
|
d);if(b)z.console&&console.log&&console.log(c);else if(b=q.ext,b=b.sErrMode||b.errMode,a&&A(a,null,"error",[a,d,c]),"alert"==b)alert(c);else{if("throw"==b)throw Error(c);"function"==typeof b&&b(a,d,c)}}function M(a,b,c,d){f.isArray(c)?f.each(c,function(c,d){f.isArray(d)?M(a,b,d[0],d[1]):M(a,b,d)}):(d===p&&(d=c),b[c]!==p&&(a[d]=b[c]))}function ab(a,b,c){var d;for(d in b)if(b.hasOwnProperty(d)){var e=b[d];f.isPlainObject(e)?(f.isPlainObject(a[d])||(a[d]={}),f.extend(!0,a[d],e)):c&&"data"!==d&&"aaData"!==
|
||||||
|
d&&f.isArray(e)?a[d]=e.slice():a[d]=e}return a}function $a(a,b,c){f(a).on("click.DT",b,function(b){f(a).blur();c(b)}).on("keypress.DT",b,function(a){13===a.which&&(a.preventDefault(),c(a))}).on("selectstart.DT",function(){return!1})}function E(a,b,c,d){c&&a[b].push({fn:c,sName:d})}function A(a,b,c,d){var e=[];b&&(e=f.map(a[b].slice().reverse(),function(b,c){return b.fn.apply(a.oInstance,d)}));null!==c&&(b=f.Event(c+".dt"),f(a.nTable).trigger(b,d),e.push(b.result));return e}function Wa(a){var b=a._iDisplayStart,
|
||||||
|
c=a.fnDisplayEnd(),d=a._iDisplayLength;b>=c&&(b=c-d);b-=b%d;if(-1===d||0>b)b=0;a._iDisplayStart=b}function Ra(a,b){a=a.renderer;var c=q.ext.renderer[b];return f.isPlainObject(a)&&a[b]?c[a[b]]||c._:"string"===typeof a?c[a]||c._:c._}function D(a){return a.oFeatures.bServerSide?"ssp":a.ajax||a.sAjaxSource?"ajax":"dom"}function ka(a,b){var c=Pb.numbers_length,d=Math.floor(c/2);b<=c?a=Z(0,b):a<=d?(a=Z(0,c-2),a.push("ellipsis"),a.push(b-1)):(a>=b-1-d?a=Z(b-(c-2),b):(a=Z(a-d+2,a+d-1),a.push("ellipsis"),
|
||||||
|
a.push(b-1)),a.splice(0,0,"ellipsis"),a.splice(0,0,0));a.DT_el="span";return a}function Ha(a){f.each({num:function(b){return Da(b,a)},"num-fmt":function(b){return Da(b,a,bb)},"html-num":function(b){return Da(b,a,Ea)},"html-num-fmt":function(b){return Da(b,a,Ea,bb)}},function(b,c){C.type.order[b+a+"-pre"]=c;b.match(/^html\-/)&&(C.type.search[b+a]=C.type.search.html)})}function Qb(a){return function(){var b=[Ca(this[q.ext.iApiIndex])].concat(Array.prototype.slice.call(arguments));return q.ext.internal[a].apply(this,
|
||||||
|
b)}}var q=function(a){this.$=function(a,b){return this.api(!0).$(a,b)};this._=function(a,b){return this.api(!0).rows(a,b).data()};this.api=function(a){return a?new v(Ca(this[C.iApiIndex])):new v(this)};this.fnAddData=function(a,b){var c=this.api(!0);a=f.isArray(a)&&(f.isArray(a[0])||f.isPlainObject(a[0]))?c.rows.add(a):c.row.add(a);(b===p||b)&&c.draw();return a.flatten().toArray()};this.fnAdjustColumnSizing=function(a){var b=this.api(!0).columns.adjust(),c=b.settings()[0],d=c.oScroll;a===p||a?b.draw(!1):
|
||||||
|
(""!==d.sX||""!==d.sY)&&na(c)};this.fnClearTable=function(a){var b=this.api(!0).clear();(a===p||a)&&b.draw()};this.fnClose=function(a){this.api(!0).row(a).child.hide()};this.fnDeleteRow=function(a,b,c){var d=this.api(!0);a=d.rows(a);var e=a.settings()[0],h=e.aoData[a[0][0]];a.remove();b&&b.call(this,e,h);(c===p||c)&&d.draw();return h};this.fnDestroy=function(a){this.api(!0).destroy(a)};this.fnDraw=function(a){this.api(!0).draw(a)};this.fnFilter=function(a,b,c,d,e,f){e=this.api(!0);null===b||b===p?
|
||||||
|
e.search(a,c,d,f):e.column(b).search(a,c,d,f);e.draw()};this.fnGetData=function(a,b){var c=this.api(!0);if(a!==p){var d=a.nodeName?a.nodeName.toLowerCase():"";return b!==p||"td"==d||"th"==d?c.cell(a,b).data():c.row(a).data()||null}return c.data().toArray()};this.fnGetNodes=function(a){var b=this.api(!0);return a!==p?b.row(a).node():b.rows().nodes().flatten().toArray()};this.fnGetPosition=function(a){var b=this.api(!0),c=a.nodeName.toUpperCase();return"TR"==c?b.row(a).index():"TD"==c||"TH"==c?(a=b.cell(a).index(),
|
||||||
|
[a.row,a.columnVisible,a.column]):null};this.fnIsOpen=function(a){return this.api(!0).row(a).child.isShown()};this.fnOpen=function(a,b,c){return this.api(!0).row(a).child(b,c).show().child()[0]};this.fnPageChange=function(a,b){a=this.api(!0).page(a);(b===p||b)&&a.draw(!1)};this.fnSetColumnVis=function(a,b,c){a=this.api(!0).column(a).visible(b);(c===p||c)&&a.columns.adjust().draw()};this.fnSettings=function(){return Ca(this[C.iApiIndex])};this.fnSort=function(a){this.api(!0).order(a).draw()};this.fnSortListener=
|
||||||
|
function(a,b,c){this.api(!0).order.listener(a,b,c)};this.fnUpdate=function(a,b,c,d,e){var h=this.api(!0);c===p||null===c?h.row(b).data(a):h.cell(b,c).data(a);(e===p||e)&&h.columns.adjust();(d===p||d)&&h.draw();return 0};this.fnVersionCheck=C.fnVersionCheck;var b=this,c=a===p,d=this.length;c&&(a={});this.oApi=this.internal=C.internal;for(var e in q.ext.internal)e&&(this[e]=Qb(e));this.each(function(){var e={},g=1<d?ab(e,a,!0):a,k=0,l;e=this.getAttribute("id");var n=!1,m=q.defaults,w=f(this);if("table"!=
|
||||||
|
this.nodeName.toLowerCase())O(null,0,"Non-table node initialisation ("+this.nodeName+")",2);else{jb(m);kb(m.column);L(m,m,!0);L(m.column,m.column,!0);L(m,f.extend(g,w.data()),!0);var u=q.settings;k=0;for(l=u.length;k<l;k++){var t=u[k];if(t.nTable==this||t.nTHead&&t.nTHead.parentNode==this||t.nTFoot&&t.nTFoot.parentNode==this){var v=g.bRetrieve!==p?g.bRetrieve:m.bRetrieve;if(c||v)return t.oInstance;if(g.bDestroy!==p?g.bDestroy:m.bDestroy){t.oInstance.fnDestroy();break}else{O(t,0,"Cannot reinitialise DataTable",
|
||||||
|
3);return}}if(t.sTableId==this.id){u.splice(k,1);break}}if(null===e||""===e)this.id=e="DataTables_Table_"+q.ext._unique++;var r=f.extend(!0,{},q.models.oSettings,{sDestroyWidth:w[0].style.width,sInstance:e,sTableId:e});r.nTable=this;r.oApi=b.internal;r.oInit=g;u.push(r);r.oInstance=1===b.length?b:w.dataTable();jb(g);Ga(g.oLanguage);g.aLengthMenu&&!g.iDisplayLength&&(g.iDisplayLength=f.isArray(g.aLengthMenu[0])?g.aLengthMenu[0][0]:g.aLengthMenu[0]);g=ab(f.extend(!0,{},m),g);M(r.oFeatures,g,"bPaginate bLengthChange bFilter bSort bSortMulti bInfo bProcessing bAutoWidth bSortClasses bServerSide bDeferRender".split(" "));
|
||||||
|
M(r,g,["asStripeClasses","ajax","fnServerData","fnFormatNumber","sServerMethod","aaSorting","aaSortingFixed","aLengthMenu","sPaginationType","sAjaxSource","sAjaxDataProp","iStateDuration","sDom","bSortCellsTop","iTabIndex","fnStateLoadCallback","fnStateSaveCallback","renderer","searchDelay","rowId",["iCookieDuration","iStateDuration"],["oSearch","oPreviousSearch"],["aoSearchCols","aoPreSearchCols"],["iDisplayLength","_iDisplayLength"]]);M(r.oScroll,g,[["sScrollX","sX"],["sScrollXInner","sXInner"],
|
||||||
|
["sScrollY","sY"],["bScrollCollapse","bCollapse"]]);M(r.oLanguage,g,"fnInfoCallback");E(r,"aoDrawCallback",g.fnDrawCallback,"user");E(r,"aoServerParams",g.fnServerParams,"user");E(r,"aoStateSaveParams",g.fnStateSaveParams,"user");E(r,"aoStateLoadParams",g.fnStateLoadParams,"user");E(r,"aoStateLoaded",g.fnStateLoaded,"user");E(r,"aoRowCallback",g.fnRowCallback,"user");E(r,"aoRowCreatedCallback",g.fnCreatedRow,"user");E(r,"aoHeaderCallback",g.fnHeaderCallback,"user");E(r,"aoFooterCallback",g.fnFooterCallback,
|
||||||
|
"user");E(r,"aoInitComplete",g.fnInitComplete,"user");E(r,"aoPreDrawCallback",g.fnPreDrawCallback,"user");r.rowIdFn=U(g.rowId);lb(r);var x=r.oClasses;f.extend(x,q.ext.classes,g.oClasses);w.addClass(x.sTable);r.iInitDisplayStart===p&&(r.iInitDisplayStart=g.iDisplayStart,r._iDisplayStart=g.iDisplayStart);null!==g.iDeferLoading&&(r.bDeferLoading=!0,e=f.isArray(g.iDeferLoading),r._iRecordsDisplay=e?g.iDeferLoading[0]:g.iDeferLoading,r._iRecordsTotal=e?g.iDeferLoading[1]:g.iDeferLoading);var y=r.oLanguage;
|
||||||
|
f.extend(!0,y,g.oLanguage);y.sUrl&&(f.ajax({dataType:"json",url:y.sUrl,success:function(a){Ga(a);L(m.oLanguage,a);f.extend(!0,y,a);ja(r)},error:function(){ja(r)}}),n=!0);null===g.asStripeClasses&&(r.asStripeClasses=[x.sStripeOdd,x.sStripeEven]);e=r.asStripeClasses;var z=w.children("tbody").find("tr").eq(0);-1!==f.inArray(!0,f.map(e,function(a,b){return z.hasClass(a)}))&&(f("tbody tr",this).removeClass(e.join(" ")),r.asDestroyStripes=e.slice());e=[];u=this.getElementsByTagName("thead");0!==u.length&&
|
||||||
|
(fa(r.aoHeader,u[0]),e=ua(r));if(null===g.aoColumns)for(u=[],k=0,l=e.length;k<l;k++)u.push(null);else u=g.aoColumns;k=0;for(l=u.length;k<l;k++)Ia(r,e?e[k]:null);nb(r,g.aoColumnDefs,u,function(a,b){ma(r,a,b)});if(z.length){var B=function(a,b){return null!==a.getAttribute("data-"+b)?b:null};f(z[0]).children("th, td").each(function(a,b){var c=r.aoColumns[a];if(c.mData===a){var d=B(b,"sort")||B(b,"order");b=B(b,"filter")||B(b,"search");if(null!==d||null!==b)c.mData={_:a+".display",sort:null!==d?a+".@data-"+
|
||||||
|
d:p,type:null!==d?a+".@data-"+d:p,filter:null!==b?a+".@data-"+b:p},ma(r,a)}})}var C=r.oFeatures;e=function(){if(g.aaSorting===p){var a=r.aaSorting;k=0;for(l=a.length;k<l;k++)a[k][1]=r.aoColumns[k].asSorting[0]}Aa(r);C.bSort&&E(r,"aoDrawCallback",function(){if(r.bSorted){var a=Y(r),b={};f.each(a,function(a,c){b[c.src]=c.dir});A(r,null,"order",[r,a,b]);Nb(r)}});E(r,"aoDrawCallback",function(){(r.bSorted||"ssp"===D(r)||C.bDeferRender)&&Aa(r)},"sc");a=w.children("caption").each(function(){this._captionSide=
|
||||||
|
f(this).css("caption-side")});var b=w.children("thead");0===b.length&&(b=f("<thead/>").appendTo(w));r.nTHead=b[0];b=w.children("tbody");0===b.length&&(b=f("<tbody/>").appendTo(w));r.nTBody=b[0];b=w.children("tfoot");0===b.length&&0<a.length&&(""!==r.oScroll.sX||""!==r.oScroll.sY)&&(b=f("<tfoot/>").appendTo(w));0===b.length||0===b.children().length?w.addClass(x.sNoFooter):0<b.length&&(r.nTFoot=b[0],fa(r.aoFooter,r.nTFoot));if(g.aaData)for(k=0;k<g.aaData.length;k++)R(r,g.aaData[k]);else(r.bDeferLoading||
|
||||||
|
"dom"==D(r))&&pa(r,f(r.nTBody).children("tr"));r.aiDisplay=r.aiDisplayMaster.slice();r.bInitialised=!0;!1===n&&ja(r)};g.bStateSave?(C.bStateSave=!0,E(r,"aoDrawCallback",Ba,"state_save"),Ob(r,g,e)):e()}});b=null;return this},C,t,x,cb={},Rb=/[\r\n\u2028]/g,Ea=/<.*?>/g,cc=/^\d{2,4}[\.\/\-]\d{1,2}[\.\/\-]\d{1,2}([T ]{1}\d{1,2}[:\.]\d{2}([\.:]\d{2})?)?$/,dc=/(\/|\.|\*|\+|\?|\||\(|\)|\[|\]|\{|\}|\\|\$|\^|\-)/g,bb=/[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi,P=function(a){return a&&!0!==a&&"-"!==a?!1:
|
||||||
|
!0},Sb=function(a){var b=parseInt(a,10);return!isNaN(b)&&isFinite(a)?b:null},Tb=function(a,b){cb[b]||(cb[b]=new RegExp(Ua(b),"g"));return"string"===typeof a&&"."!==b?a.replace(/\./g,"").replace(cb[b],"."):a},db=function(a,b,c){var d="string"===typeof a;if(P(a))return!0;b&&d&&(a=Tb(a,b));c&&d&&(a=a.replace(bb,""));return!isNaN(parseFloat(a))&&isFinite(a)},Ub=function(a,b,c){return P(a)?!0:P(a)||"string"===typeof a?db(a.replace(Ea,""),b,c)?!0:null:null},J=function(a,b,c){var d=[],e=0,h=a.length;if(c!==
|
||||||
|
p)for(;e<h;e++)a[e]&&a[e][b]&&d.push(a[e][b][c]);else for(;e<h;e++)a[e]&&d.push(a[e][b]);return d},la=function(a,b,c,d){var e=[],h=0,g=b.length;if(d!==p)for(;h<g;h++)a[b[h]][c]&&e.push(a[b[h]][c][d]);else for(;h<g;h++)e.push(a[b[h]][c]);return e},Z=function(a,b){var c=[];if(b===p){b=0;var d=a}else d=b,b=a;for(a=b;a<d;a++)c.push(a);return c},Vb=function(a){for(var b=[],c=0,d=a.length;c<d;c++)a[c]&&b.push(a[c]);return b},ta=function(a){a:{if(!(2>a.length)){var b=a.slice().sort();for(var c=b[0],d=1,
|
||||||
|
e=b.length;d<e;d++){if(b[d]===c){b=!1;break a}c=b[d]}}b=!0}if(b)return a.slice();b=[];e=a.length;var h,g=0;d=0;a:for(;d<e;d++){c=a[d];for(h=0;h<g;h++)if(b[h]===c)continue a;b.push(c);g++}return b};q.util={throttle:function(a,b){var c=b!==p?b:200,d,e;return function(){var b=this,g=+new Date,f=arguments;d&&g<d+c?(clearTimeout(e),e=setTimeout(function(){d=p;a.apply(b,f)},c)):(d=g,a.apply(b,f))}},escapeRegex:function(a){return a.replace(dc,"\\$1")}};var F=function(a,b,c){a[b]!==p&&(a[c]=a[b])},da=/\[.*?\]$/,
|
||||||
|
X=/\(\)$/,Ua=q.util.escapeRegex,ya=f("<div>")[0],$b=ya.textContent!==p,bc=/<.*?>/g,Sa=q.util.throttle,Wb=[],G=Array.prototype,ec=function(a){var b,c=q.settings,d=f.map(c,function(a,b){return a.nTable});if(a){if(a.nTable&&a.oApi)return[a];if(a.nodeName&&"table"===a.nodeName.toLowerCase()){var e=f.inArray(a,d);return-1!==e?[c[e]]:null}if(a&&"function"===typeof a.settings)return a.settings().toArray();"string"===typeof a?b=f(a):a instanceof f&&(b=a)}else return[];if(b)return b.map(function(a){e=f.inArray(this,
|
||||||
|
d);return-1!==e?c[e]:null}).toArray()};var v=function(a,b){if(!(this instanceof v))return new v(a,b);var c=[],d=function(a){(a=ec(a))&&c.push.apply(c,a)};if(f.isArray(a))for(var e=0,h=a.length;e<h;e++)d(a[e]);else d(a);this.context=ta(c);b&&f.merge(this,b);this.selector={rows:null,cols:null,opts:null};v.extend(this,this,Wb)};q.Api=v;f.extend(v.prototype,{any:function(){return 0!==this.count()},concat:G.concat,context:[],count:function(){return this.flatten().length},each:function(a){for(var b=0,c=
|
||||||
|
this.length;b<c;b++)a.call(this,this[b],b,this);return this},eq:function(a){var b=this.context;return b.length>a?new v(b[a],this[a]):null},filter:function(a){var b=[];if(G.filter)b=G.filter.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)a.call(this,this[c],c,this)&&b.push(this[c]);return new v(this.context,b)},flatten:function(){var a=[];return new v(this.context,a.concat.apply(a,this.toArray()))},join:G.join,indexOf:G.indexOf||function(a,b){b=b||0;for(var c=this.length;b<c;b++)if(this[b]===
|
||||||
|
a)return b;return-1},iterator:function(a,b,c,d){var e=[],h,g,f=this.context,l,n=this.selector;"string"===typeof a&&(d=c,c=b,b=a,a=!1);var m=0;for(h=f.length;m<h;m++){var q=new v(f[m]);if("table"===b){var u=c.call(q,f[m],m);u!==p&&e.push(u)}else if("columns"===b||"rows"===b)u=c.call(q,f[m],this[m],m),u!==p&&e.push(u);else if("column"===b||"column-rows"===b||"row"===b||"cell"===b){var t=this[m];"column-rows"===b&&(l=Fa(f[m],n.opts));var x=0;for(g=t.length;x<g;x++)u=t[x],u="cell"===b?c.call(q,f[m],u.row,
|
||||||
|
u.column,m,x):c.call(q,f[m],u,m,x,l),u!==p&&e.push(u)}}return e.length||d?(a=new v(f,a?e.concat.apply([],e):e),b=a.selector,b.rows=n.rows,b.cols=n.cols,b.opts=n.opts,a):this},lastIndexOf:G.lastIndexOf||function(a,b){return this.indexOf.apply(this.toArray.reverse(),arguments)},length:0,map:function(a){var b=[];if(G.map)b=G.map.call(this,a,this);else for(var c=0,d=this.length;c<d;c++)b.push(a.call(this,this[c],c));return new v(this.context,b)},pluck:function(a){return this.map(function(b){return b[a]})},
|
||||||
|
pop:G.pop,push:G.push,reduce:G.reduce||function(a,b){return mb(this,a,b,0,this.length,1)},reduceRight:G.reduceRight||function(a,b){return mb(this,a,b,this.length-1,-1,-1)},reverse:G.reverse,selector:null,shift:G.shift,slice:function(){return new v(this.context,this)},sort:G.sort,splice:G.splice,toArray:function(){return G.slice.call(this)},to$:function(){return f(this)},toJQuery:function(){return f(this)},unique:function(){return new v(this.context,ta(this))},unshift:G.unshift});v.extend=function(a,
|
||||||
|
b,c){if(c.length&&b&&(b instanceof v||b.__dt_wrapper)){var d,e=function(a,b,c){return function(){var d=b.apply(a,arguments);v.extend(d,d,c.methodExt);return d}};var h=0;for(d=c.length;h<d;h++){var g=c[h];b[g.name]="function"===g.type?e(a,g.val,g):"object"===g.type?{}:g.val;b[g.name].__dt_wrapper=!0;v.extend(a,b[g.name],g.propExt)}}};v.register=t=function(a,b){if(f.isArray(a))for(var c=0,d=a.length;c<d;c++)v.register(a[c],b);else{d=a.split(".");var e=Wb,h;a=0;for(c=d.length;a<c;a++){var g=(h=-1!==
|
||||||
|
d[a].indexOf("()"))?d[a].replace("()",""):d[a];a:{var k=0;for(var l=e.length;k<l;k++)if(e[k].name===g){k=e[k];break a}k=null}k||(k={name:g,val:{},methodExt:[],propExt:[],type:"object"},e.push(k));a===c-1?(k.val=b,k.type="function"===typeof b?"function":f.isPlainObject(b)?"object":"other"):e=h?k.methodExt:k.propExt}}};v.registerPlural=x=function(a,b,c){v.register(a,c);v.register(b,function(){var a=c.apply(this,arguments);return a===this?this:a instanceof v?a.length?f.isArray(a[0])?new v(a.context,
|
||||||
|
a[0]):a[0]:p:a})};var fc=function(a,b){if("number"===typeof a)return[b[a]];var c=f.map(b,function(a,b){return a.nTable});return f(c).filter(a).map(function(a){a=f.inArray(this,c);return b[a]}).toArray()};t("tables()",function(a){return a?new v(fc(a,this.context)):this});t("table()",function(a){a=this.tables(a);var b=a.context;return b.length?new v(b[0]):a});x("tables().nodes()","table().node()",function(){return this.iterator("table",function(a){return a.nTable},1)});x("tables().body()","table().body()",
|
||||||
|
function(){return this.iterator("table",function(a){return a.nTBody},1)});x("tables().header()","table().header()",function(){return this.iterator("table",function(a){return a.nTHead},1)});x("tables().footer()","table().footer()",function(){return this.iterator("table",function(a){return a.nTFoot},1)});x("tables().containers()","table().container()",function(){return this.iterator("table",function(a){return a.nTableWrapper},1)});t("draw()",function(a){return this.iterator("table",function(b){"page"===
|
||||||
|
a?S(b):("string"===typeof a&&(a="full-hold"===a?!1:!0),V(b,!1===a))})});t("page()",function(a){return a===p?this.page.info().page:this.iterator("table",function(b){Xa(b,a)})});t("page.info()",function(a){if(0===this.context.length)return p;a=this.context[0];var b=a._iDisplayStart,c=a.oFeatures.bPaginate?a._iDisplayLength:-1,d=a.fnRecordsDisplay(),e=-1===c;return{page:e?0:Math.floor(b/c),pages:e?1:Math.ceil(d/c),start:b,end:a.fnDisplayEnd(),length:c,recordsTotal:a.fnRecordsTotal(),recordsDisplay:d,
|
||||||
|
serverSide:"ssp"===D(a)}});t("page.len()",function(a){return a===p?0!==this.context.length?this.context[0]._iDisplayLength:p:this.iterator("table",function(b){Va(b,a)})});var Xb=function(a,b,c){if(c){var d=new v(a);d.one("draw",function(){c(d.ajax.json())})}if("ssp"==D(a))V(a,b);else{K(a,!0);var e=a.jqXHR;e&&4!==e.readyState&&e.abort();va(a,[],function(c){qa(a);c=wa(a,c);for(var d=0,e=c.length;d<e;d++)R(a,c[d]);V(a,b);K(a,!1)})}};t("ajax.json()",function(){var a=this.context;if(0<a.length)return a[0].json});
|
||||||
|
t("ajax.params()",function(){var a=this.context;if(0<a.length)return a[0].oAjaxData});t("ajax.reload()",function(a,b){return this.iterator("table",function(c){Xb(c,!1===b,a)})});t("ajax.url()",function(a){var b=this.context;if(a===p){if(0===b.length)return p;b=b[0];return b.ajax?f.isPlainObject(b.ajax)?b.ajax.url:b.ajax:b.sAjaxSource}return this.iterator("table",function(b){f.isPlainObject(b.ajax)?b.ajax.url=a:b.ajax=a})});t("ajax.url().load()",function(a,b){return this.iterator("table",function(c){Xb(c,
|
||||||
|
!1===b,a)})});var eb=function(a,b,c,d,e){var h=[],g,k,l;var n=typeof b;b&&"string"!==n&&"function"!==n&&b.length!==p||(b=[b]);n=0;for(k=b.length;n<k;n++){var m=b[n]&&b[n].split&&!b[n].match(/[\[\(:]/)?b[n].split(","):[b[n]];var q=0;for(l=m.length;q<l;q++)(g=c("string"===typeof m[q]?f.trim(m[q]):m[q]))&&g.length&&(h=h.concat(g))}a=C.selector[a];if(a.length)for(n=0,k=a.length;n<k;n++)h=a[n](d,e,h);return ta(h)},fb=function(a){a||(a={});a.filter&&a.search===p&&(a.search=a.filter);return f.extend({search:"none",
|
||||||
|
order:"current",page:"all"},a)},gb=function(a){for(var b=0,c=a.length;b<c;b++)if(0<a[b].length)return a[0]=a[b],a[0].length=1,a.length=1,a.context=[a.context[b]],a;a.length=0;return a},Fa=function(a,b){var c=[],d=a.aiDisplay;var e=a.aiDisplayMaster;var h=b.search;var g=b.order;b=b.page;if("ssp"==D(a))return"removed"===h?[]:Z(0,e.length);if("current"==b)for(g=a._iDisplayStart,a=a.fnDisplayEnd();g<a;g++)c.push(d[g]);else if("current"==g||"applied"==g)if("none"==h)c=e.slice();else if("applied"==h)c=
|
||||||
|
d.slice();else{if("removed"==h){var k={};g=0;for(a=d.length;g<a;g++)k[d[g]]=null;c=f.map(e,function(a){return k.hasOwnProperty(a)?null:a})}}else if("index"==g||"original"==g)for(g=0,a=a.aoData.length;g<a;g++)"none"==h?c.push(g):(e=f.inArray(g,d),(-1===e&&"removed"==h||0<=e&&"applied"==h)&&c.push(g));return c},gc=function(a,b,c){var d;return eb("row",b,function(b){var e=Sb(b),g=a.aoData;if(null!==e&&!c)return[e];d||(d=Fa(a,c));if(null!==e&&-1!==f.inArray(e,d))return[e];if(null===b||b===p||""===b)return d;
|
||||||
|
if("function"===typeof b)return f.map(d,function(a){var c=g[a];return b(a,c._aData,c.nTr)?a:null});if(b.nodeName){e=b._DT_RowIndex;var k=b._DT_CellIndex;if(e!==p)return g[e]&&g[e].nTr===b?[e]:[];if(k)return g[k.row]&&g[k.row].nTr===b.parentNode?[k.row]:[];e=f(b).closest("*[data-dt-row]");return e.length?[e.data("dt-row")]:[]}if("string"===typeof b&&"#"===b.charAt(0)&&(e=a.aIds[b.replace(/^#/,"")],e!==p))return[e.idx];e=Vb(la(a.aoData,d,"nTr"));return f(e).filter(b).map(function(){return this._DT_RowIndex}).toArray()},
|
||||||
|
a,c)};t("rows()",function(a,b){a===p?a="":f.isPlainObject(a)&&(b=a,a="");b=fb(b);var c=this.iterator("table",function(c){return gc(c,a,b)},1);c.selector.rows=a;c.selector.opts=b;return c});t("rows().nodes()",function(){return this.iterator("row",function(a,b){return a.aoData[b].nTr||p},1)});t("rows().data()",function(){return this.iterator(!0,"rows",function(a,b){return la(a.aoData,b,"_aData")},1)});x("rows().cache()","row().cache()",function(a){return this.iterator("row",function(b,c){b=b.aoData[c];
|
||||||
|
return"search"===a?b._aFilterData:b._aSortData},1)});x("rows().invalidate()","row().invalidate()",function(a){return this.iterator("row",function(b,c){ea(b,c,a)})});x("rows().indexes()","row().index()",function(){return this.iterator("row",function(a,b){return b},1)});x("rows().ids()","row().id()",function(a){for(var b=[],c=this.context,d=0,e=c.length;d<e;d++)for(var h=0,g=this[d].length;h<g;h++){var f=c[d].rowIdFn(c[d].aoData[this[d][h]]._aData);b.push((!0===a?"#":"")+f)}return new v(c,b)});x("rows().remove()",
|
||||||
|
"row().remove()",function(){var a=this;this.iterator("row",function(b,c,d){var e=b.aoData,h=e[c],g,f;e.splice(c,1);var l=0;for(g=e.length;l<g;l++){var n=e[l];var m=n.anCells;null!==n.nTr&&(n.nTr._DT_RowIndex=l);if(null!==m)for(n=0,f=m.length;n<f;n++)m[n]._DT_CellIndex.row=l}ra(b.aiDisplayMaster,c);ra(b.aiDisplay,c);ra(a[d],c,!1);0<b._iRecordsDisplay&&b._iRecordsDisplay--;Wa(b);c=b.rowIdFn(h._aData);c!==p&&delete b.aIds[c]});this.iterator("table",function(a){for(var b=0,d=a.aoData.length;b<d;b++)a.aoData[b].idx=
|
||||||
|
b});return this});t("rows.add()",function(a){var b=this.iterator("table",function(b){var c,d=[];var g=0;for(c=a.length;g<c;g++){var f=a[g];f.nodeName&&"TR"===f.nodeName.toUpperCase()?d.push(pa(b,f)[0]):d.push(R(b,f))}return d},1),c=this.rows(-1);c.pop();f.merge(c,b);return c});t("row()",function(a,b){return gb(this.rows(a,b))});t("row().data()",function(a){var b=this.context;if(a===p)return b.length&&this.length?b[0].aoData[this[0]]._aData:p;var c=b[0].aoData[this[0]];c._aData=a;f.isArray(a)&&c.nTr.id&&
|
||||||
|
Q(b[0].rowId)(a,c.nTr.id);ea(b[0],this[0],"data");return this});t("row().node()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]].nTr||null:null});t("row.add()",function(a){a instanceof f&&a.length&&(a=a[0]);var b=this.iterator("table",function(b){return a.nodeName&&"TR"===a.nodeName.toUpperCase()?pa(b,a)[0]:R(b,a)});return this.row(b[0])});var hc=function(a,b,c,d){var e=[],h=function(b,c){if(f.isArray(b)||b instanceof f)for(var d=0,g=b.length;d<g;d++)h(b[d],c);else b.nodeName&&
|
||||||
|
"tr"===b.nodeName.toLowerCase()?e.push(b):(d=f("<tr><td/></tr>").addClass(c),f("td",d).addClass(c).html(b)[0].colSpan=W(a),e.push(d[0]))};h(c,d);b._details&&b._details.detach();b._details=f(e);b._detailsShow&&b._details.insertAfter(b.nTr)},hb=function(a,b){var c=a.context;c.length&&(a=c[0].aoData[b!==p?b:a[0]])&&a._details&&(a._details.remove(),a._detailsShow=p,a._details=p)},Yb=function(a,b){var c=a.context;c.length&&a.length&&(a=c[0].aoData[a[0]],a._details&&((a._detailsShow=b)?a._details.insertAfter(a.nTr):
|
||||||
|
a._details.detach(),ic(c[0])))},ic=function(a){var b=new v(a),c=a.aoData;b.off("draw.dt.DT_details column-visibility.dt.DT_details destroy.dt.DT_details");0<J(c,"_details").length&&(b.on("draw.dt.DT_details",function(d,e){a===e&&b.rows({page:"current"}).eq(0).each(function(a){a=c[a];a._detailsShow&&a._details.insertAfter(a.nTr)})}),b.on("column-visibility.dt.DT_details",function(b,e,f,g){if(a===e)for(e=W(e),f=0,g=c.length;f<g;f++)b=c[f],b._details&&b._details.children("td[colspan]").attr("colspan",
|
||||||
|
e)}),b.on("destroy.dt.DT_details",function(d,e){if(a===e)for(d=0,e=c.length;d<e;d++)c[d]._details&&hb(b,d)}))};t("row().child()",function(a,b){var c=this.context;if(a===p)return c.length&&this.length?c[0].aoData[this[0]]._details:p;!0===a?this.child.show():!1===a?hb(this):c.length&&this.length&&hc(c[0],c[0].aoData[this[0]],a,b);return this});t(["row().child.show()","row().child().show()"],function(a){Yb(this,!0);return this});t(["row().child.hide()","row().child().hide()"],function(){Yb(this,!1);
|
||||||
|
return this});t(["row().child.remove()","row().child().remove()"],function(){hb(this);return this});t("row().child.isShown()",function(){var a=this.context;return a.length&&this.length?a[0].aoData[this[0]]._detailsShow||!1:!1});var jc=/^([^:]+):(name|visIdx|visible)$/,Zb=function(a,b,c,d,e){c=[];d=0;for(var f=e.length;d<f;d++)c.push(I(a,e[d],b));return c},kc=function(a,b,c){var d=a.aoColumns,e=J(d,"sName"),h=J(d,"nTh");return eb("column",b,function(b){var g=Sb(b);if(""===b)return Z(d.length);if(null!==
|
||||||
|
g)return[0<=g?g:d.length+g];if("function"===typeof b){var l=Fa(a,c);return f.map(d,function(c,d){return b(d,Zb(a,d,0,0,l),h[d])?d:null})}var n="string"===typeof b?b.match(jc):"";if(n)switch(n[2]){case "visIdx":case "visible":g=parseInt(n[1],10);if(0>g){var m=f.map(d,function(a,b){return a.bVisible?b:null});return[m[m.length+g]]}return[ba(a,g)];case "name":return f.map(e,function(a,b){return a===n[1]?b:null});default:return[]}if(b.nodeName&&b._DT_CellIndex)return[b._DT_CellIndex.column];g=f(h).filter(b).map(function(){return f.inArray(this,
|
||||||
|
h)}).toArray();if(g.length||!b.nodeName)return g;g=f(b).closest("*[data-dt-column]");return g.length?[g.data("dt-column")]:[]},a,c)};t("columns()",function(a,b){a===p?a="":f.isPlainObject(a)&&(b=a,a="");b=fb(b);var c=this.iterator("table",function(c){return kc(c,a,b)},1);c.selector.cols=a;c.selector.opts=b;return c});x("columns().header()","column().header()",function(a,b){return this.iterator("column",function(a,b){return a.aoColumns[b].nTh},1)});x("columns().footer()","column().footer()",function(a,
|
||||||
|
b){return this.iterator("column",function(a,b){return a.aoColumns[b].nTf},1)});x("columns().data()","column().data()",function(){return this.iterator("column-rows",Zb,1)});x("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(a,b){return a.aoColumns[b].mData},1)});x("columns().cache()","column().cache()",function(a){return this.iterator("column-rows",function(b,c,d,e,f){return la(b.aoData,f,"search"===a?"_aFilterData":"_aSortData",c)},1)});x("columns().nodes()",
|
||||||
|
"column().nodes()",function(){return this.iterator("column-rows",function(a,b,c,d,e){return la(a.aoData,e,"anCells",b)},1)});x("columns().visible()","column().visible()",function(a,b){var c=this,d=this.iterator("column",function(b,c){if(a===p)return b.aoColumns[c].bVisible;var d=b.aoColumns,e=d[c],h=b.aoData,n;if(a!==p&&e.bVisible!==a){if(a){var m=f.inArray(!0,J(d,"bVisible"),c+1);d=0;for(n=h.length;d<n;d++){var q=h[d].nTr;b=h[d].anCells;q&&q.insertBefore(b[c],b[m]||null)}}else f(J(b.aoData,"anCells",
|
||||||
|
c)).detach();e.bVisible=a}});a!==p&&this.iterator("table",function(d){ha(d,d.aoHeader);ha(d,d.aoFooter);d.aiDisplay.length||f(d.nTBody).find("td[colspan]").attr("colspan",W(d));Ba(d);c.iterator("column",function(c,d){A(c,null,"column-visibility",[c,d,a,b])});(b===p||b)&&c.columns.adjust()});return d});x("columns().indexes()","column().index()",function(a){return this.iterator("column",function(b,c){return"visible"===a?ca(b,c):c},1)});t("columns.adjust()",function(){return this.iterator("table",function(a){aa(a)},
|
||||||
|
1)});t("column.index()",function(a,b){if(0!==this.context.length){var c=this.context[0];if("fromVisible"===a||"toData"===a)return ba(c,b);if("fromData"===a||"toVisible"===a)return ca(c,b)}});t("column()",function(a,b){return gb(this.columns(a,b))});var lc=function(a,b,c){var d=a.aoData,e=Fa(a,c),h=Vb(la(d,e,"anCells")),g=f([].concat.apply([],h)),k,l=a.aoColumns.length,n,m,q,u,t,v;return eb("cell",b,function(b){var c="function"===typeof b;if(null===b||b===p||c){n=[];m=0;for(q=e.length;m<q;m++)for(k=
|
||||||
|
e[m],u=0;u<l;u++)t={row:k,column:u},c?(v=d[k],b(t,I(a,k,u),v.anCells?v.anCells[u]:null)&&n.push(t)):n.push(t);return n}if(f.isPlainObject(b))return b.column!==p&&b.row!==p&&-1!==f.inArray(b.row,e)?[b]:[];c=g.filter(b).map(function(a,b){return{row:b._DT_CellIndex.row,column:b._DT_CellIndex.column}}).toArray();if(c.length||!b.nodeName)return c;v=f(b).closest("*[data-dt-row]");return v.length?[{row:v.data("dt-row"),column:v.data("dt-column")}]:[]},a,c)};t("cells()",function(a,b,c){f.isPlainObject(a)&&
|
||||||
|
(a.row===p?(c=a,a=null):(c=b,b=null));f.isPlainObject(b)&&(c=b,b=null);if(null===b||b===p)return this.iterator("table",function(b){return lc(b,a,fb(c))});var d=c?{page:c.page,order:c.order,search:c.search}:{},e=this.columns(b,d),h=this.rows(a,d),g,k,l,n;d=this.iterator("table",function(a,b){a=[];g=0;for(k=h[b].length;g<k;g++)for(l=0,n=e[b].length;l<n;l++)a.push({row:h[b][g],column:e[b][l]});return a},1);d=c&&c.selected?this.cells(d,c):d;f.extend(d.selector,{cols:b,rows:a,opts:c});return d});x("cells().nodes()",
|
||||||
|
"cell().node()",function(){return this.iterator("cell",function(a,b,c){return(a=a.aoData[b])&&a.anCells?a.anCells[c]:p},1)});t("cells().data()",function(){return this.iterator("cell",function(a,b,c){return I(a,b,c)},1)});x("cells().cache()","cell().cache()",function(a){a="search"===a?"_aFilterData":"_aSortData";return this.iterator("cell",function(b,c,d){return b.aoData[c][a][d]},1)});x("cells().render()","cell().render()",function(a){return this.iterator("cell",function(b,c,d){return I(b,c,d,a)},
|
||||||
|
1)});x("cells().indexes()","cell().index()",function(){return this.iterator("cell",function(a,b,c){return{row:b,column:c,columnVisible:ca(a,c)}},1)});x("cells().invalidate()","cell().invalidate()",function(a){return this.iterator("cell",function(b,c,d){ea(b,c,a,d)})});t("cell()",function(a,b,c){return gb(this.cells(a,b,c))});t("cell().data()",function(a){var b=this.context,c=this[0];if(a===p)return b.length&&c.length?I(b[0],c[0].row,c[0].column):p;ob(b[0],c[0].row,c[0].column,a);ea(b[0],c[0].row,
|
||||||
|
"data",c[0].column);return this});t("order()",function(a,b){var c=this.context;if(a===p)return 0!==c.length?c[0].aaSorting:p;"number"===typeof a?a=[[a,b]]:a.length&&!f.isArray(a[0])&&(a=Array.prototype.slice.call(arguments));return this.iterator("table",function(b){b.aaSorting=a.slice()})});t("order.listener()",function(a,b,c){return this.iterator("table",function(d){Qa(d,a,b,c)})});t("order.fixed()",function(a){if(!a){var b=this.context;b=b.length?b[0].aaSortingFixed:p;return f.isArray(b)?{pre:b}:
|
||||||
|
b}return this.iterator("table",function(b){b.aaSortingFixed=f.extend(!0,{},a)})});t(["columns().order()","column().order()"],function(a){var b=this;return this.iterator("table",function(c,d){var e=[];f.each(b[d],function(b,c){e.push([c,a])});c.aaSorting=e})});t("search()",function(a,b,c,d){var e=this.context;return a===p?0!==e.length?e[0].oPreviousSearch.sSearch:p:this.iterator("table",function(e){e.oFeatures.bFilter&&ia(e,f.extend({},e.oPreviousSearch,{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===
|
||||||
|
c?!0:c,bCaseInsensitive:null===d?!0:d}),1)})});x("columns().search()","column().search()",function(a,b,c,d){return this.iterator("column",function(e,h){var g=e.aoPreSearchCols;if(a===p)return g[h].sSearch;e.oFeatures.bFilter&&(f.extend(g[h],{sSearch:a+"",bRegex:null===b?!1:b,bSmart:null===c?!0:c,bCaseInsensitive:null===d?!0:d}),ia(e,e.oPreviousSearch,1))})});t("state()",function(){return this.context.length?this.context[0].oSavedState:null});t("state.clear()",function(){return this.iterator("table",
|
||||||
|
function(a){a.fnStateSaveCallback.call(a.oInstance,a,{})})});t("state.loaded()",function(){return this.context.length?this.context[0].oLoadedState:null});t("state.save()",function(){return this.iterator("table",function(a){Ba(a)})});q.versionCheck=q.fnVersionCheck=function(a){var b=q.version.split(".");a=a.split(".");for(var c,d,e=0,f=a.length;e<f;e++)if(c=parseInt(b[e],10)||0,d=parseInt(a[e],10)||0,c!==d)return c>d;return!0};q.isDataTable=q.fnIsDataTable=function(a){var b=f(a).get(0),c=!1;if(a instanceof
|
||||||
|
q.Api)return!0;f.each(q.settings,function(a,e){a=e.nScrollHead?f("table",e.nScrollHead)[0]:null;var d=e.nScrollFoot?f("table",e.nScrollFoot)[0]:null;if(e.nTable===b||a===b||d===b)c=!0});return c};q.tables=q.fnTables=function(a){var b=!1;f.isPlainObject(a)&&(b=a.api,a=a.visible);var c=f.map(q.settings,function(b){if(!a||a&&f(b.nTable).is(":visible"))return b.nTable});return b?new v(c):c};q.camelToHungarian=L;t("$()",function(a,b){b=this.rows(b).nodes();b=f(b);return f([].concat(b.filter(a).toArray(),
|
||||||
|
b.find(a).toArray()))});f.each(["on","one","off"],function(a,b){t(b+"()",function(){var a=Array.prototype.slice.call(arguments);a[0]=f.map(a[0].split(/\s/),function(a){return a.match(/\.dt\b/)?a:a+".dt"}).join(" ");var d=f(this.tables().nodes());d[b].apply(d,a);return this})});t("clear()",function(){return this.iterator("table",function(a){qa(a)})});t("settings()",function(){return new v(this.context,this.context)});t("init()",function(){var a=this.context;return a.length?a[0].oInit:null});t("data()",
|
||||||
|
function(){return this.iterator("table",function(a){return J(a.aoData,"_aData")}).flatten()});t("destroy()",function(a){a=a||!1;return this.iterator("table",function(b){var c=b.nTableWrapper.parentNode,d=b.oClasses,e=b.nTable,h=b.nTBody,g=b.nTHead,k=b.nTFoot,l=f(e);h=f(h);var n=f(b.nTableWrapper),m=f.map(b.aoData,function(a){return a.nTr}),p;b.bDestroying=!0;A(b,"aoDestroyCallback","destroy",[b]);a||(new v(b)).columns().visible(!0);n.off(".DT").find(":not(tbody *)").off(".DT");f(z).off(".DT-"+b.sInstance);
|
||||||
|
e!=g.parentNode&&(l.children("thead").detach(),l.append(g));k&&e!=k.parentNode&&(l.children("tfoot").detach(),l.append(k));b.aaSorting=[];b.aaSortingFixed=[];Aa(b);f(m).removeClass(b.asStripeClasses.join(" "));f("th, td",g).removeClass(d.sSortable+" "+d.sSortableAsc+" "+d.sSortableDesc+" "+d.sSortableNone);h.children().detach();h.append(m);g=a?"remove":"detach";l[g]();n[g]();!a&&c&&(c.insertBefore(e,b.nTableReinsertBefore),l.css("width",b.sDestroyWidth).removeClass(d.sTable),(p=b.asDestroyStripes.length)&&
|
||||||
|
h.children().each(function(a){f(this).addClass(b.asDestroyStripes[a%p])}));c=f.inArray(b,q.settings);-1!==c&&q.settings.splice(c,1)})});f.each(["column","row","cell"],function(a,b){t(b+"s().every()",function(a){var c=this.selector.opts,e=this;return this.iterator(b,function(d,f,k,l,n){a.call(e[b](f,"cell"===b?k:c,"cell"===b?c:p),f,k,l,n)})})});t("i18n()",function(a,b,c){var d=this.context[0];a=U(a)(d.oLanguage);a===p&&(a=b);c!==p&&f.isPlainObject(a)&&(a=a[c]!==p?a[c]:a._);return a.replace("%d",c)});
|
||||||
|
q.version="1.10.20";q.settings=[];q.models={};q.models.oSearch={bCaseInsensitive:!0,sSearch:"",bRegex:!1,bSmart:!0};q.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,_sRowStripe:"",src:null,idx:-1};q.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,nTh:null,nTf:null,sClass:null,sContentPadding:null,
|
||||||
|
sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sSortingClassJUI:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null};q.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],asStripeClasses:null,bAutoWidth:!0,bDeferRender:!1,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:!1,
|
||||||
|
bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(a){return a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnServerData:null,fnServerParams:null,fnStateLoadCallback:function(a){try{return JSON.parse((-1===a.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+a.sInstance+"_"+location.pathname))}catch(b){}},
|
||||||
|
fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(a,b){try{(-1===a.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+a.sInstance+"_"+location.pathname,JSON.stringify(b))}catch(c){}},fnStateSaveParams:null,iStateDuration:7200,iDeferLoading:null,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{sSortAscending:": activate to sort column ascending",sSortDescending:": activate to sort column descending"},oPaginate:{sFirst:"First",sLast:"Last",
|
||||||
|
sNext:"Next",sPrevious:"Previous"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ entries",sInfoEmpty:"Showing 0 to 0 of 0 entries",sInfoFiltered:"(filtered from _MAX_ total entries)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"Show _MENU_ entries",sLoadingRecords:"Loading...",sProcessing:"Processing...",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},oSearch:f.extend({},q.models.oSearch),sAjaxDataProp:"data",
|
||||||
|
sAjaxSource:null,sDom:"lfrtip",searchDelay:null,sPaginationType:"simple_numbers",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId"};H(q.defaults);q.defaults.column={aDataSort:null,iDataSort:-1,asSorting:["asc","desc"],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null};H(q.defaults.column);q.models.oSettings=
|
||||||
|
{oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:null,bLengthChange:null,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollOversize:!1,bScrollbarLeft:!1,bBounding:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},
|
||||||
|
aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],asStripeClasses:null,asDestroyStripes:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bDeferLoading:!1,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",iStateDuration:0,
|
||||||
|
aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,sAjaxSource:null,sAjaxDataProp:null,bAjaxDataGet:!0,jqXHR:null,json:p,oAjaxData:p,fnServerData:null,aoServerParams:[],sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==D(this)?1*this._iRecordsTotal:
|
||||||
|
this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==D(this)?1*this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var a=this._iDisplayLength,b=this._iDisplayStart,c=b+a,d=this.aiDisplay.length,e=this.oFeatures,f=e.bPaginate;return e.bServerSide?!1===f||-1===a?b+d:Math.min(b+a,this._iRecordsDisplay):!f||c>d||-1===a?d:c},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null};q.ext=C={buttons:{},
|
||||||
|
classes:{},builder:"-source-",errMode:"alert",feature:[],search:[],selector:{cell:[],column:[],row:[]},internal:{},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{detect:[],search:{},order:{}},_unique:0,fnVersionCheck:q.fnVersionCheck,iApiIndex:0,oJUIClasses:{},sVersion:q.version};f.extend(C,{afnFiltering:C.search,aTypes:C.type.detect,ofnSearch:C.type.search,oSort:C.type.order,afnSortData:C.order,aoFeatures:C.feature,oApi:C.internal,oStdClasses:C.classes,oPagination:C.pager});
|
||||||
|
f.extend(q.ext.classes,{sTable:"dataTable",sNoFooter:"no-footer",sPageButton:"paginate_button",sPageButtonActive:"current",sPageButtonDisabled:"disabled",sStripeOdd:"odd",sStripeEven:"even",sRowEmpty:"dataTables_empty",sWrapper:"dataTables_wrapper",sFilter:"dataTables_filter",sInfo:"dataTables_info",sPaging:"dataTables_paginate paging_",sLength:"dataTables_length",sProcessing:"dataTables_processing",sSortAsc:"sorting_asc",sSortDesc:"sorting_desc",sSortable:"sorting",sSortableAsc:"sorting_asc_disabled",
|
||||||
|
sSortableDesc:"sorting_desc_disabled",sSortableNone:"sorting_disabled",sSortColumn:"sorting_",sFilterInput:"",sLengthSelect:"",sScrollWrapper:"dataTables_scroll",sScrollHead:"dataTables_scrollHead",sScrollHeadInner:"dataTables_scrollHeadInner",sScrollBody:"dataTables_scrollBody",sScrollFoot:"dataTables_scrollFoot",sScrollFootInner:"dataTables_scrollFootInner",sHeaderTH:"",sFooterTH:"",sSortJUIAsc:"",sSortJUIDesc:"",sSortJUI:"",sSortJUIAscAllowed:"",sSortJUIDescAllowed:"",sSortJUIWrapper:"",sSortIcon:"",
|
||||||
|
sJUIHeader:"",sJUIFooter:""});var Pb=q.ext.pager;f.extend(Pb,{simple:function(a,b){return["previous","next"]},full:function(a,b){return["first","previous","next","last"]},numbers:function(a,b){return[ka(a,b)]},simple_numbers:function(a,b){return["previous",ka(a,b),"next"]},full_numbers:function(a,b){return["first","previous",ka(a,b),"next","last"]},first_last_numbers:function(a,b){return["first",ka(a,b),"last"]},_numbers:ka,numbers_length:7});f.extend(!0,q.ext.renderer,{pageButton:{_:function(a,b,
|
||||||
|
c,d,e,h){var g=a.oClasses,k=a.oLanguage.oPaginate,l=a.oLanguage.oAria.paginate||{},n,m,q=0,t=function(b,d){var p,r=g.sPageButtonDisabled,u=function(b){Xa(a,b.data.action,!0)};var w=0;for(p=d.length;w<p;w++){var v=d[w];if(f.isArray(v)){var x=f("<"+(v.DT_el||"div")+"/>").appendTo(b);t(x,v)}else{n=null;m=v;x=a.iTabIndex;switch(v){case "ellipsis":b.append('<span class="ellipsis">…</span>');break;case "first":n=k.sFirst;0===e&&(x=-1,m+=" "+r);break;case "previous":n=k.sPrevious;0===e&&(x=-1,m+=
|
||||||
|
" "+r);break;case "next":n=k.sNext;e===h-1&&(x=-1,m+=" "+r);break;case "last":n=k.sLast;e===h-1&&(x=-1,m+=" "+r);break;default:n=v+1,m=e===v?g.sPageButtonActive:""}null!==n&&(x=f("<a>",{"class":g.sPageButton+" "+m,"aria-controls":a.sTableId,"aria-label":l[v],"data-dt-idx":q,tabindex:x,id:0===c&&"string"===typeof v?a.sTableId+"_"+v:null}).html(n).appendTo(b),$a(x,{action:v},u),q++)}}};try{var v=f(b).find(y.activeElement).data("dt-idx")}catch(mc){}t(f(b).empty(),d);v!==p&&f(b).find("[data-dt-idx="+
|
||||||
|
v+"]").focus()}}});f.extend(q.ext.type.detect,[function(a,b){b=b.oLanguage.sDecimal;return db(a,b)?"num"+b:null},function(a,b){if(a&&!(a instanceof Date)&&!cc.test(a))return null;b=Date.parse(a);return null!==b&&!isNaN(b)||P(a)?"date":null},function(a,b){b=b.oLanguage.sDecimal;return db(a,b,!0)?"num-fmt"+b:null},function(a,b){b=b.oLanguage.sDecimal;return Ub(a,b)?"html-num"+b:null},function(a,b){b=b.oLanguage.sDecimal;return Ub(a,b,!0)?"html-num-fmt"+b:null},function(a,b){return P(a)||"string"===
|
||||||
|
typeof a&&-1!==a.indexOf("<")?"html":null}]);f.extend(q.ext.type.search,{html:function(a){return P(a)?a:"string"===typeof a?a.replace(Rb," ").replace(Ea,""):""},string:function(a){return P(a)?a:"string"===typeof a?a.replace(Rb," "):a}});var Da=function(a,b,c,d){if(0!==a&&(!a||"-"===a))return-Infinity;b&&(a=Tb(a,b));a.replace&&(c&&(a=a.replace(c,"")),d&&(a=a.replace(d,"")));return 1*a};f.extend(C.type.order,{"date-pre":function(a){a=Date.parse(a);return isNaN(a)?-Infinity:a},"html-pre":function(a){return P(a)?
|
||||||
|
"":a.replace?a.replace(/<.*?>/g,"").toLowerCase():a+""},"string-pre":function(a){return P(a)?"":"string"===typeof a?a.toLowerCase():a.toString?a.toString():""},"string-asc":function(a,b){return a<b?-1:a>b?1:0},"string-desc":function(a,b){return a<b?1:a>b?-1:0}});Ha("");f.extend(!0,q.ext.renderer,{header:{_:function(a,b,c,d){f(a.nTable).on("order.dt.DT",function(e,f,g,k){a===f&&(e=c.idx,b.removeClass(c.sSortingClass+" "+d.sSortAsc+" "+d.sSortDesc).addClass("asc"==k[e]?d.sSortAsc:"desc"==k[e]?d.sSortDesc:
|
||||||
|
c.sSortingClass))})},jqueryui:function(a,b,c,d){f("<div/>").addClass(d.sSortJUIWrapper).append(b.contents()).append(f("<span/>").addClass(d.sSortIcon+" "+c.sSortingClassJUI)).appendTo(b);f(a.nTable).on("order.dt.DT",function(e,f,g,k){a===f&&(e=c.idx,b.removeClass(d.sSortAsc+" "+d.sSortDesc).addClass("asc"==k[e]?d.sSortAsc:"desc"==k[e]?d.sSortDesc:c.sSortingClass),b.find("span."+d.sSortIcon).removeClass(d.sSortJUIAsc+" "+d.sSortJUIDesc+" "+d.sSortJUI+" "+d.sSortJUIAscAllowed+" "+d.sSortJUIDescAllowed).addClass("asc"==
|
||||||
|
k[e]?d.sSortJUIAsc:"desc"==k[e]?d.sSortJUIDesc:c.sSortingClassJUI))})}}});var ib=function(a){return"string"===typeof a?a.replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""):a};q.render={number:function(a,b,c,d,e){return{display:function(f){if("number"!==typeof f&&"string"!==typeof f)return f;var g=0>f?"-":"",h=parseFloat(f);if(isNaN(h))return ib(f);h=h.toFixed(c);f=Math.abs(h);h=parseInt(f,10);f=c?b+(f-h).toFixed(c).substring(2):"";return g+(d||"")+h.toString().replace(/\B(?=(\d{3})+(?!\d))/g,
|
||||||
|
a)+f+(e||"")}}},text:function(){return{display:ib,filter:ib}}};f.extend(q.ext.internal,{_fnExternApiFunc:Qb,_fnBuildAjax:va,_fnAjaxUpdate:qb,_fnAjaxParameters:zb,_fnAjaxUpdateDraw:Ab,_fnAjaxDataSrc:wa,_fnAddColumn:Ia,_fnColumnOptions:ma,_fnAdjustColumnSizing:aa,_fnVisibleToColumnIndex:ba,_fnColumnIndexToVisible:ca,_fnVisbleColumns:W,_fnGetColumns:oa,_fnColumnTypes:Ka,_fnApplyColumnDefs:nb,_fnHungarianMap:H,_fnCamelToHungarian:L,_fnLanguageCompat:Ga,_fnBrowserDetect:lb,_fnAddData:R,_fnAddTr:pa,_fnNodeToDataIndex:function(a,
|
||||||
|
b){return b._DT_RowIndex!==p?b._DT_RowIndex:null},_fnNodeToColumnIndex:function(a,b,c){return f.inArray(c,a.aoData[b].anCells)},_fnGetCellData:I,_fnSetCellData:ob,_fnSplitObjNotation:Na,_fnGetObjectDataFn:U,_fnSetObjectDataFn:Q,_fnGetDataMaster:Oa,_fnClearTable:qa,_fnDeleteIndex:ra,_fnInvalidate:ea,_fnGetRowElements:Ma,_fnCreateTr:La,_fnBuildHead:pb,_fnDrawHead:ha,_fnDraw:S,_fnReDraw:V,_fnAddOptionsHtml:sb,_fnDetectHeader:fa,_fnGetUniqueThs:ua,_fnFeatureHtmlFilter:ub,_fnFilterComplete:ia,_fnFilterCustom:Db,
|
||||||
|
_fnFilterColumn:Cb,_fnFilter:Bb,_fnFilterCreateSearch:Ta,_fnEscapeRegex:Ua,_fnFilterData:Eb,_fnFeatureHtmlInfo:xb,_fnUpdateInfo:Hb,_fnInfoMacros:Ib,_fnInitialise:ja,_fnInitComplete:xa,_fnLengthChange:Va,_fnFeatureHtmlLength:tb,_fnFeatureHtmlPaginate:yb,_fnPageChange:Xa,_fnFeatureHtmlProcessing:vb,_fnProcessingDisplay:K,_fnFeatureHtmlTable:wb,_fnScrollDraw:na,_fnApplyToChildren:N,_fnCalculateColumnWidths:Ja,_fnThrottle:Sa,_fnConvertToWidth:Jb,_fnGetWidestNode:Kb,_fnGetMaxLenString:Lb,_fnStringToCss:B,
|
||||||
|
_fnSortFlatten:Y,_fnSort:rb,_fnSortAria:Nb,_fnSortListener:Za,_fnSortAttachListener:Qa,_fnSortingClasses:Aa,_fnSortData:Mb,_fnSaveState:Ba,_fnLoadState:Ob,_fnSettingsFromNode:Ca,_fnLog:O,_fnMap:M,_fnBindAction:$a,_fnCallbackReg:E,_fnCallbackFire:A,_fnLengthOverflow:Wa,_fnRenderer:Ra,_fnDataSource:D,_fnRowAttributes:Pa,_fnExtend:ab,_fnCalculateEnd:function(){}});f.fn.dataTable=q;q.$=f;f.fn.dataTableSettings=q.settings;f.fn.dataTableExt=q.ext;f.fn.DataTable=function(a){return f(this).dataTable(a).api()};
|
||||||
|
f.each(q,function(a,b){f.fn.DataTable[a]=b});return f.fn.dataTable});
|
1
static/vendor/datatables/select.bootstrap4.min.css
vendored
Normal file
1
static/vendor/datatables/select.bootstrap4.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
table.dataTable tbody>tr.selected,table.dataTable tbody>tr>.selected{background-color:#0275d8}table.dataTable.stripe tbody>tr.odd.selected,table.dataTable.stripe tbody>tr.odd>.selected,table.dataTable.display tbody>tr.odd.selected,table.dataTable.display tbody>tr.odd>.selected{background-color:#0272d3}table.dataTable.hover tbody>tr.selected:hover,table.dataTable.hover tbody>tr>.selected:hover,table.dataTable.display tbody>tr.selected:hover,table.dataTable.display tbody>tr>.selected:hover{background-color:#0271d0}table.dataTable.order-column tbody>tr.selected>.sorting_1,table.dataTable.order-column tbody>tr.selected>.sorting_2,table.dataTable.order-column tbody>tr.selected>.sorting_3,table.dataTable.order-column tbody>tr>.selected,table.dataTable.display tbody>tr.selected>.sorting_1,table.dataTable.display tbody>tr.selected>.sorting_2,table.dataTable.display tbody>tr.selected>.sorting_3,table.dataTable.display tbody>tr>.selected{background-color:#0273d4}table.dataTable.display tbody>tr.odd.selected>.sorting_1,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_1{background-color:#026fcc}table.dataTable.display tbody>tr.odd.selected>.sorting_2,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_2{background-color:#0270ce}table.dataTable.display tbody>tr.odd.selected>.sorting_3,table.dataTable.order-column.stripe tbody>tr.odd.selected>.sorting_3{background-color:#0270d0}table.dataTable.display tbody>tr.even.selected>.sorting_1,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_1{background-color:#0273d4}table.dataTable.display tbody>tr.even.selected>.sorting_2,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_2{background-color:#0274d5}table.dataTable.display tbody>tr.even.selected>.sorting_3,table.dataTable.order-column.stripe tbody>tr.even.selected>.sorting_3{background-color:#0275d7}table.dataTable.display tbody>tr.odd>.selected,table.dataTable.order-column.stripe tbody>tr.odd>.selected{background-color:#026fcc}table.dataTable.display tbody>tr.even>.selected,table.dataTable.order-column.stripe tbody>tr.even>.selected{background-color:#0273d4}table.dataTable.display tbody>tr.selected:hover>.sorting_1,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_1{background-color:#026bc6}table.dataTable.display tbody>tr.selected:hover>.sorting_2,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_2{background-color:#026cc8}table.dataTable.display tbody>tr.selected:hover>.sorting_3,table.dataTable.order-column.hover tbody>tr.selected:hover>.sorting_3{background-color:#026eca}table.dataTable.display tbody>tr:hover>.selected,table.dataTable.display tbody>tr>.selected:hover,table.dataTable.order-column.hover tbody>tr:hover>.selected,table.dataTable.order-column.hover tbody>tr>.selected:hover{background-color:#026bc6}table.dataTable tbody td.select-checkbox,table.dataTable tbody th.select-checkbox{position:relative}table.dataTable tbody td.select-checkbox:before,table.dataTable tbody td.select-checkbox:after,table.dataTable tbody th.select-checkbox:before,table.dataTable tbody th.select-checkbox:after{display:block;position:absolute;top:1.2em;left:50%;width:12px;height:12px;box-sizing:border-box}table.dataTable tbody td.select-checkbox:before,table.dataTable tbody th.select-checkbox:before{content:' ';margin-top:-6px;margin-left:-6px;border:1px solid black;border-radius:3px}table.dataTable tr.selected td.select-checkbox:after,table.dataTable tr.selected th.select-checkbox:after{content:'\2714';margin-top:-11px;margin-left:-4px;text-align:center;text-shadow:1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9}div.dataTables_wrapper span.select-info,div.dataTables_wrapper span.select-item{margin-left:0.5em}@media screen and (max-width: 640px){div.dataTables_wrapper span.select-info,div.dataTables_wrapper span.select-item{margin-left:0;display:block}}table.dataTable tbody tr.selected,table.dataTable tbody th.selected,table.dataTable tbody td.selected{color:white}table.dataTable tbody tr.selected a,table.dataTable tbody th.selected a,table.dataTable tbody td.selected a{color:#a2d4ed}
|
5
static/vendor/datatables/select.bootstrap4.min.js
vendored
Normal file
5
static/vendor/datatables/select.bootstrap4.min.js
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/*!
|
||||||
|
Bootstrap 4 styling wrapper for Select
|
||||||
|
©2018 SpryMedia Ltd - datatables.net/license
|
||||||
|
*/
|
||||||
|
(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net-bs4","datatables.net-select"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,b){a||(a=window);b&&b.fn.dataTable||(b=require("datatables.net-bs4")(a,b).$);b.fn.dataTable.select||require("datatables.net-select")(a,b);return c(b,a,a.document)}:c(jQuery,window,document)})(function(c,a,b,d){return c.fn.dataTable});
|
5
static/vendor/fontawesome-free/css/all.min.css
vendored
Normal file
5
static/vendor/fontawesome-free/css/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
81
static/vendor/fontawesome-free/package.json
vendored
Normal file
81
static/vendor/fontawesome-free/package.json
vendored
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
{
|
||||||
|
"_from": "@fortawesome/fontawesome-free@5.10.2",
|
||||||
|
"_id": "@fortawesome/fontawesome-free@5.10.2",
|
||||||
|
"_inBundle": false,
|
||||||
|
"_integrity": "sha512-9pw+Nsnunl9unstGEHQ+u41wBEQue6XPBsILXtJF/4fNN1L3avJcMF/gGF86rIjeTAgfLjTY9ndm68/X4f4idQ==",
|
||||||
|
"_location": "/@fortawesome/fontawesome-free",
|
||||||
|
"_phantomChildren": {},
|
||||||
|
"_requested": {
|
||||||
|
"type": "version",
|
||||||
|
"registry": true,
|
||||||
|
"raw": "@fortawesome/fontawesome-free@5.10.2",
|
||||||
|
"name": "@fortawesome/fontawesome-free",
|
||||||
|
"escapedName": "@fortawesome%2ffontawesome-free",
|
||||||
|
"scope": "@fortawesome",
|
||||||
|
"rawSpec": "5.10.2",
|
||||||
|
"saveSpec": null,
|
||||||
|
"fetchSpec": "5.10.2"
|
||||||
|
},
|
||||||
|
"_requiredBy": [
|
||||||
|
"/"
|
||||||
|
],
|
||||||
|
"_resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.10.2.tgz",
|
||||||
|
"_shasum": "27e02da1e34b50c9869179d364fb46627b521130",
|
||||||
|
"_spec": "@fortawesome/fontawesome-free@5.10.2",
|
||||||
|
"_where": "/Users/DANGER_DAVID/Sites/startbootstrap-themes/startbootstrap-sb-admin-2",
|
||||||
|
"author": {
|
||||||
|
"name": "Dave Gandy",
|
||||||
|
"email": "dave@fontawesome.com",
|
||||||
|
"url": "http://twitter.com/davegandy"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "http://github.com/FortAwesome/Font-Awesome/issues"
|
||||||
|
},
|
||||||
|
"bundleDependencies": false,
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"name": "Brian Talbot",
|
||||||
|
"url": "http://twitter.com/talbs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Travis Chase",
|
||||||
|
"url": "http://twitter.com/supercodepoet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Rob Madole",
|
||||||
|
"url": "http://twitter.com/robmadole"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Geremia Taglialatela",
|
||||||
|
"url": "http://twitter.com/gtagliala"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mike Wilkerson",
|
||||||
|
"url": "http://twitter.com/mw77"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {},
|
||||||
|
"deprecated": false,
|
||||||
|
"description": "The iconic font, CSS, and SVG framework",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"homepage": "https://fontawesome.com",
|
||||||
|
"keywords": [
|
||||||
|
"font",
|
||||||
|
"awesome",
|
||||||
|
"fontawesome",
|
||||||
|
"icon",
|
||||||
|
"svg",
|
||||||
|
"bootstrap"
|
||||||
|
],
|
||||||
|
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
|
||||||
|
"main": "js/fontawesome.js",
|
||||||
|
"name": "@fortawesome/fontawesome-free",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/FortAwesome/Font-Awesome.git"
|
||||||
|
},
|
||||||
|
"style": "css/fontawesome.css",
|
||||||
|
"version": "5.10.2"
|
||||||
|
}
|
1
static/vendor/fontawesome-free/svgs/solid/exchange-alt.svg
vendored
Normal file
1
static/vendor/fontawesome-free/svgs/solid/exchange-alt.svg
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M0 168v-16c0-13.255 10.745-24 24-24h360V80c0-21.367 25.899-32.042 40.971-16.971l80 80c9.372 9.373 9.372 24.569 0 33.941l-80 80C409.956 271.982 384 261.456 384 240v-48H24c-13.255 0-24-10.745-24-24zm488 152H128v-48c0-21.314-25.862-32.08-40.971-16.971l-80 80c-9.372 9.373-9.372 24.569 0 33.941l80 80C102.057 463.997 128 453.437 128 432v-48h360c13.255 0 24-10.745 24-24v-16c0-13.255-10.745-24-24-24z"/></svg>
|
After Width: | Height: | Size: 475 B |
1
static/vendor/fontawesome-free/svgs/solid/folder-open.svg
vendored
Normal file
1
static/vendor/fontawesome-free/svgs/solid/folder-open.svg
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M572.694 292.093L500.27 416.248A63.997 63.997 0 0 1 444.989 448H45.025c-18.523 0-30.064-20.093-20.731-36.093l72.424-124.155A64 64 0 0 1 152 256h399.964c18.523 0 30.064 20.093 20.73 36.093zM152 224h328v-48c0-26.51-21.49-48-48-48H272l-64-64H48C21.49 64 0 85.49 0 112v278.046l69.077-118.418C86.214 242.25 117.989 224 152 224z"/></svg>
|
After Width: | Height: | Size: 402 B |
1
static/vendor/fontawesome-free/svgs/solid/user.svg
vendored
Normal file
1
static/vendor/fontawesome-free/svgs/solid/user.svg
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm89.6 32h-16.7c-22.2 10.2-46.9 16-72.9 16s-50.6-5.8-72.9-16h-16.7C60.2 288 0 348.2 0 422.4V464c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48v-41.6c0-74.2-60.2-134.4-134.4-134.4z"/></svg>
|
After Width: | Height: | Size: 336 B |
BIN
static/vendor/fontawesome-free/webfonts/fa-solid-900.eot
vendored
Normal file
BIN
static/vendor/fontawesome-free/webfonts/fa-solid-900.eot
vendored
Normal file
Binary file not shown.
4649
static/vendor/fontawesome-free/webfonts/fa-solid-900.svg
vendored
Normal file
4649
static/vendor/fontawesome-free/webfonts/fa-solid-900.svg
vendored
Normal file
File diff suppressed because it is too large
Load diff
After Width: | Height: | Size: 820 KiB |
BIN
static/vendor/fontawesome-free/webfonts/fa-solid-900.ttf
vendored
Normal file
BIN
static/vendor/fontawesome-free/webfonts/fa-solid-900.ttf
vendored
Normal file
Binary file not shown.
BIN
static/vendor/fontawesome-free/webfonts/fa-solid-900.woff
vendored
Normal file
BIN
static/vendor/fontawesome-free/webfonts/fa-solid-900.woff
vendored
Normal file
Binary file not shown.
BIN
static/vendor/fontawesome-free/webfonts/fa-solid-900.woff2
vendored
Normal file
BIN
static/vendor/fontawesome-free/webfonts/fa-solid-900.woff2
vendored
Normal file
Binary file not shown.
59
static/vendor/jquery-easing/jquery.easing.compatibility.js
vendored
Normal file
59
static/vendor/jquery-easing/jquery.easing.compatibility.js
vendored
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Easing Compatibility v1 - http://gsgd.co.uk/sandbox/jquery/easing
|
||||||
|
*
|
||||||
|
* Adds compatibility for applications that use the pre 1.2 easing names
|
||||||
|
*
|
||||||
|
* Copyright (c) 2007 George Smith
|
||||||
|
* Licensed under the MIT License:
|
||||||
|
* http://www.opensource.org/licenses/mit-license.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($){
|
||||||
|
$.extend( $.easing,
|
||||||
|
{
|
||||||
|
easeIn: function (x, t, b, c, d) {
|
||||||
|
return $.easing.easeInQuad(x, t, b, c, d);
|
||||||
|
},
|
||||||
|
easeOut: function (x, t, b, c, d) {
|
||||||
|
return $.easing.easeOutQuad(x, t, b, c, d);
|
||||||
|
},
|
||||||
|
easeInOut: function (x, t, b, c, d) {
|
||||||
|
return $.easing.easeInOutQuad(x, t, b, c, d);
|
||||||
|
},
|
||||||
|
expoin: function(x, t, b, c, d) {
|
||||||
|
return $.easing.easeInExpo(x, t, b, c, d);
|
||||||
|
},
|
||||||
|
expoout: function(x, t, b, c, d) {
|
||||||
|
return $.easing.easeOutExpo(x, t, b, c, d);
|
||||||
|
},
|
||||||
|
expoinout: function(x, t, b, c, d) {
|
||||||
|
return $.easing.easeInOutExpo(x, t, b, c, d);
|
||||||
|
},
|
||||||
|
bouncein: function(x, t, b, c, d) {
|
||||||
|
return $.easing.easeInBounce(x, t, b, c, d);
|
||||||
|
},
|
||||||
|
bounceout: function(x, t, b, c, d) {
|
||||||
|
return $.easing.easeOutBounce(x, t, b, c, d);
|
||||||
|
},
|
||||||
|
bounceinout: function(x, t, b, c, d) {
|
||||||
|
return $.easing.easeInOutBounce(x, t, b, c, d);
|
||||||
|
},
|
||||||
|
elasin: function(x, t, b, c, d) {
|
||||||
|
return $.easing.easeInElastic(x, t, b, c, d);
|
||||||
|
},
|
||||||
|
elasout: function(x, t, b, c, d) {
|
||||||
|
return $.easing.easeOutElastic(x, t, b, c, d);
|
||||||
|
},
|
||||||
|
elasinout: function(x, t, b, c, d) {
|
||||||
|
return $.easing.easeInOutElastic(x, t, b, c, d);
|
||||||
|
},
|
||||||
|
backin: function(x, t, b, c, d) {
|
||||||
|
return $.easing.easeInBack(x, t, b, c, d);
|
||||||
|
},
|
||||||
|
backout: function(x, t, b, c, d) {
|
||||||
|
return $.easing.easeOutBack(x, t, b, c, d);
|
||||||
|
},
|
||||||
|
backinout: function(x, t, b, c, d) {
|
||||||
|
return $.easing.easeInOutBack(x, t, b, c, d);
|
||||||
|
}
|
||||||
|
});})(jQuery);
|
1
static/vendor/jquery-easing/jquery.easing.min.js
vendored
Normal file
1
static/vendor/jquery-easing/jquery.easing.min.js
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
(function(factory){if(typeof define==="function"&&define.amd){define(["jquery"],function($){return factory($)})}else if(typeof module==="object"&&typeof module.exports==="object"){exports=factory(require("jquery"))}else{factory(jQuery)}})(function($){$.easing.jswing=$.easing.swing;var pow=Math.pow,sqrt=Math.sqrt,sin=Math.sin,cos=Math.cos,PI=Math.PI,c1=1.70158,c2=c1*1.525,c3=c1+1,c4=2*PI/3,c5=2*PI/4.5;function bounceOut(x){var n1=7.5625,d1=2.75;if(x<1/d1){return n1*x*x}else if(x<2/d1){return n1*(x-=1.5/d1)*x+.75}else if(x<2.5/d1){return n1*(x-=2.25/d1)*x+.9375}else{return n1*(x-=2.625/d1)*x+.984375}}$.extend($.easing,{def:"easeOutQuad",swing:function(x){return $.easing[$.easing.def](x)},easeInQuad:function(x){return x*x},easeOutQuad:function(x){return 1-(1-x)*(1-x)},easeInOutQuad:function(x){return x<.5?2*x*x:1-pow(-2*x+2,2)/2},easeInCubic:function(x){return x*x*x},easeOutCubic:function(x){return 1-pow(1-x,3)},easeInOutCubic:function(x){return x<.5?4*x*x*x:1-pow(-2*x+2,3)/2},easeInQuart:function(x){return x*x*x*x},easeOutQuart:function(x){return 1-pow(1-x,4)},easeInOutQuart:function(x){return x<.5?8*x*x*x*x:1-pow(-2*x+2,4)/2},easeInQuint:function(x){return x*x*x*x*x},easeOutQuint:function(x){return 1-pow(1-x,5)},easeInOutQuint:function(x){return x<.5?16*x*x*x*x*x:1-pow(-2*x+2,5)/2},easeInSine:function(x){return 1-cos(x*PI/2)},easeOutSine:function(x){return sin(x*PI/2)},easeInOutSine:function(x){return-(cos(PI*x)-1)/2},easeInExpo:function(x){return x===0?0:pow(2,10*x-10)},easeOutExpo:function(x){return x===1?1:1-pow(2,-10*x)},easeInOutExpo:function(x){return x===0?0:x===1?1:x<.5?pow(2,20*x-10)/2:(2-pow(2,-20*x+10))/2},easeInCirc:function(x){return 1-sqrt(1-pow(x,2))},easeOutCirc:function(x){return sqrt(1-pow(x-1,2))},easeInOutCirc:function(x){return x<.5?(1-sqrt(1-pow(2*x,2)))/2:(sqrt(1-pow(-2*x+2,2))+1)/2},easeInElastic:function(x){return x===0?0:x===1?1:-pow(2,10*x-10)*sin((x*10-10.75)*c4)},easeOutElastic:function(x){return x===0?0:x===1?1:pow(2,-10*x)*sin((x*10-.75)*c4)+1},easeInOutElastic:function(x){return x===0?0:x===1?1:x<.5?-(pow(2,20*x-10)*sin((20*x-11.125)*c5))/2:pow(2,-20*x+10)*sin((20*x-11.125)*c5)/2+1},easeInBack:function(x){return c3*x*x*x-c1*x*x},easeOutBack:function(x){return 1+c3*pow(x-1,3)+c1*pow(x-1,2)},easeInOutBack:function(x){return x<.5?pow(2*x,2)*((c2+1)*2*x-c2)/2:(pow(2*x-2,2)*((c2+1)*(x*2-2)+c2)+2)/2},easeInBounce:function(x){return 1-bounceOut(1-x)},easeOutBounce:bounceOut,easeInOutBounce:function(x){return x<.5?(1-bounceOut(1-2*x))/2:(1+bounceOut(2*x-1))/2}})});
|
2
static/vendor/jquery/jquery.min.js
vendored
Normal file
2
static/vendor/jquery/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
static/vendor/jquery/jquery.slim.min.js
vendored
Normal file
2
static/vendor/jquery/jquery.slim.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
131
templates/base.html
Normal file
131
templates/base.html
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
{{define "base"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="author" content="">
|
||||||
|
|
||||||
|
<title>SFTPGo - {{template "title" .}}</title>
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/static/favicon.ico" />
|
||||||
|
|
||||||
|
<!-- Custom fonts for this template-->
|
||||||
|
<link href="/static/vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
|
||||||
|
<link href="/static/css/fonts.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom styles for this template-->
|
||||||
|
<link href="/static/css/sb-admin-2.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
div.dt-buttons {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-form-error {
|
||||||
|
color: var(--red) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{{block "extra_css" .}}{{end}}
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="page-top">
|
||||||
|
|
||||||
|
<!-- Page Wrapper -->
|
||||||
|
<div id="wrapper">
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
|
||||||
|
|
||||||
|
<!-- Sidebar - Brand -->
|
||||||
|
<a class="sidebar-brand d-flex align-items-center justify-content-center" href="{{.UsersURL}}">
|
||||||
|
<div class="sidebar-brand-icon">
|
||||||
|
<i class="fas fa-folder-open"></i>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-brand-text mx-3" style="text-transform: none;">SFTPGo Web</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<hr class="sidebar-divider my-0">
|
||||||
|
|
||||||
|
|
||||||
|
<li class="nav-item {{if eq .CurrentURL .UsersURL}}active{{end}}">
|
||||||
|
<a class="nav-link" href="{{.UsersURL}}">
|
||||||
|
<i class="fas fa-fw fa-user"></i>
|
||||||
|
<span>{{.UsersTitle}}</span></a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item {{if eq .CurrentURL .ConnectionsURL}}active{{end}}">
|
||||||
|
<a class="nav-link" href="{{.ConnectionsURL}}">
|
||||||
|
<i class="fas fa-exchange-alt"></i>
|
||||||
|
<span>{{.ConnectionsTitle}}</span></a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<hr class="sidebar-divider d-none d-md-block">
|
||||||
|
|
||||||
|
<!-- Sidebar Toggler (Sidebar) -->
|
||||||
|
<div class="text-center d-none d-md-inline">
|
||||||
|
<button class="rounded-circle border-0" id="sidebarToggle"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<!-- End of Sidebar -->
|
||||||
|
|
||||||
|
<!-- Content Wrapper -->
|
||||||
|
<div id="content-wrapper" class="d-flex flex-column">
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div id="content">
|
||||||
|
|
||||||
|
<!-- Topbar -->
|
||||||
|
<nav class="mb-4 static-top">
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
<!-- End of Topbar -->
|
||||||
|
|
||||||
|
<!-- Begin Page Content -->
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
{{template "page_body" .}}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- /.container-fluid -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- End of Main Content -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- End of Content Wrapper -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- End of Page Wrapper -->
|
||||||
|
|
||||||
|
<!-- Scroll to Top Button-->
|
||||||
|
<a class="scroll-to-top rounded" href="#page-top">
|
||||||
|
<i class="fas fa-angle-up"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{{block "dialog" .}}{{end}}
|
||||||
|
|
||||||
|
<!-- Bootstrap core JavaScript-->
|
||||||
|
<script src="/static/vendor/jquery/jquery.min.js"></script>
|
||||||
|
<script src="/static/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Core plugin JavaScript-->
|
||||||
|
<script src="/static/vendor/jquery-easing/jquery.easing.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom scripts for all pages-->
|
||||||
|
<script src="/static/js/sb-admin-2.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Page level plugins -->
|
||||||
|
{{block "extra_js" .}}{{end}}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
{{end}}
|
161
templates/connections.html
Normal file
161
templates/connections.html
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "title"}}{{.Title}}{{end}}
|
||||||
|
|
||||||
|
{{define "extra_css"}}
|
||||||
|
<link href="/static/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
|
||||||
|
<link href="/static/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
|
||||||
|
<link href="/static/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "page_body"}}
|
||||||
|
<div id="errorMsg" class="card mb-4 border-left-warning" style="display: none;">
|
||||||
|
<div id="errorTxt" class="card-body text-form-error"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .Connections}}
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">View and manage connections</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-bordered" id="dataTable" width="100%" cellspacing="0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Info</th>
|
||||||
|
<th>Transfers</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Connections}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.ConnectionID}}</td>
|
||||||
|
<td>{{.Username}}</td>
|
||||||
|
<td>{{.GetConnectionDuration}}</td>
|
||||||
|
<td>{{.GetConnectionInfo}}</td>
|
||||||
|
<td>{{.GetTransfersAsString}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="card mb-4 border-left-success">
|
||||||
|
<div class="card-body">No user connected</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "dialog"}}
|
||||||
|
<div class="modal fade" id="disconnectModal" tabindex="-1" role="dialog" aria-labelledby="disconnectModalLabel"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="disconnectModalLabel">
|
||||||
|
Confirmation required
|
||||||
|
</h5>
|
||||||
|
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">Do you want to close the selected connection?</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" type="button" data-dismiss="modal">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-warning" href="#" onclick="disconnectAction()">
|
||||||
|
Disconnect
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "extra_js"}}
|
||||||
|
<script src="/static/vendor/datatables/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="/static/vendor/datatables/dataTables.bootstrap4.min.js"></script>
|
||||||
|
<script src="/static/vendor/datatables/dataTables.select.min.js"></script>
|
||||||
|
<script src="/static/vendor/datatables/select.bootstrap4.min.js"></script>
|
||||||
|
<script src="/static/vendor/datatables/dataTables.buttons.min.js"></script>
|
||||||
|
<script src="/static/vendor/datatables/buttons.bootstrap4.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
function disconnectAction() {
|
||||||
|
var table = $('#dataTable').DataTable();
|
||||||
|
table.button(0).enable(false);
|
||||||
|
var connectionID = table.row({ selected: true }).data()[0];
|
||||||
|
var path = '{{.APIConnectionsURL}}'.trimEnd("/") + "/" + connectionID;
|
||||||
|
$('#disconnectModal').modal('hide');
|
||||||
|
$.ajax({
|
||||||
|
url: path,
|
||||||
|
type: 'DELETE',
|
||||||
|
dataType: 'json',
|
||||||
|
timeout: 15000,
|
||||||
|
success: function (result) {
|
||||||
|
setTimeout(function () {
|
||||||
|
table.button(0).enable(true);
|
||||||
|
window.location.href = '{{.ConnectionsURL}}';
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
error: function ($xhr, textStatus, errorThrown) {
|
||||||
|
table.button(0).enable(true);
|
||||||
|
var txt = "Unable to close the selected connection";
|
||||||
|
if ($xhr) {
|
||||||
|
var json = $xhr.responseJSON;
|
||||||
|
if (json) {
|
||||||
|
txt += ": " + json.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$('#errorTxt').text(txt);
|
||||||
|
$('#errorMsg').show();
|
||||||
|
setTimeout(function () {
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
$.fn.dataTable.ext.buttons.disconnect = {
|
||||||
|
text: 'Disconnect',
|
||||||
|
action: function (e, dt, node, config) {
|
||||||
|
$('#disconnectModal').modal('show');
|
||||||
|
},
|
||||||
|
enabled: false
|
||||||
|
};
|
||||||
|
|
||||||
|
var table = $('#dataTable').DataTable({
|
||||||
|
dom: "<'row'<'col-sm-12'B>>" +
|
||||||
|
"<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
|
||||||
|
"<'row'<'col-sm-12'tr>>" +
|
||||||
|
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||||
|
select: true,
|
||||||
|
buttons: [
|
||||||
|
'disconnect'
|
||||||
|
],
|
||||||
|
"columnDefs": [
|
||||||
|
{
|
||||||
|
"targets": [0],
|
||||||
|
"visible": false,
|
||||||
|
"searchable": false
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"scrollX": false,
|
||||||
|
"order": [[1, 'asc']]
|
||||||
|
});
|
||||||
|
|
||||||
|
table.on('select deselect', function () {
|
||||||
|
var selectedRows = table.rows({ selected: true }).count();
|
||||||
|
table.button(0).enable(selectedRows == 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
19
templates/message.html
Normal file
19
templates/message.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "title"}}{{.Title}}{{end}}
|
||||||
|
|
||||||
|
{{define "page_body"}}
|
||||||
|
<h1 class="h5 mb-4 text-gray-800">{{.Title}}</h1>
|
||||||
|
{{if .Error}}
|
||||||
|
<div class="card mb-4 border-left-warning">
|
||||||
|
<div class="card-body text-form-error">{{.Error}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Success}}
|
||||||
|
<div class="card mb-4 border-left-success">
|
||||||
|
<div class="card-body">{{.Success}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{end}}
|
135
templates/user.html
Normal file
135
templates/user.html
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "title"}}{{.Title}}{{end}}
|
||||||
|
|
||||||
|
{{define "page_body"}}
|
||||||
|
|
||||||
|
<!-- Page Heading -->
|
||||||
|
<h1 class="h5 mb-4 text-gray-800">{{if .IsAdd}}Add a new user{{else}}Edit user{{end}}</h1>
|
||||||
|
{{if .Error}}
|
||||||
|
<div class="card mb-4 border-left-warning">
|
||||||
|
<div class="card-body text-form-error">{{.Error}}</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<form action="{{.CurrentURL}}" method="POST" autocomplete="off">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idUsername" class="col-sm-2 col-form-label">Username</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" id="idUsername" name="username" placeholder=""
|
||||||
|
value="{{.User.Username}}" maxlength="255" autocomplete="nope" required
|
||||||
|
{{if not .IsAdd}}readonly{{end}}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idPassword" class="col-sm-2 col-form-label">Password</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="password" class="form-control" id="idPassword" name="password" placeholder="" maxlength="255"
|
||||||
|
autocomplete="new-password" {{if not .IsAdd}}aria-describedby="pwdHelpBlock" {{end}}>
|
||||||
|
{{if not .IsAdd}}
|
||||||
|
<small id="pwdHelpBlock" class="form-text text-muted">
|
||||||
|
If empty the current password will not be changed
|
||||||
|
</small>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idPublicKeys" class="col-sm-2 col-form-label">Public keys</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<textarea class="form-control" id="idPublicKeys" name="public_keys" rows="3"
|
||||||
|
aria-describedby="pkHelpBlock">{{range .User.PublicKeys}}{{.}} {{end}}</textarea>
|
||||||
|
<small id="pkHelpBlock" class="form-text text-muted">
|
||||||
|
One public key per line
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idPermissions" class="col-sm-2 col-form-label">Permissions</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<select class="form-control id=" idPermissions" name="permissions" required multiple>
|
||||||
|
{{range $validPerm := .ValidPerms}}
|
||||||
|
<option value="{{$validPerm}}"
|
||||||
|
{{range $perm := $.User.Permissions}}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}
|
||||||
|
</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idHomeDir" class="col-sm-2 col-form-label">Home Dir</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" id="idHomeDir" name="home_dir" placeholder=""
|
||||||
|
value="{{.User.HomeDir}}" maxlength="255">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idQuotaFiles" class="col-sm-2 col-form-label">Quota files</label>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<input type="number" class="form-control" id="idQuotaFiles" name="quota_files" placeholder=""
|
||||||
|
value="{{.User.QuotaFiles}}" min="0" aria-describedby="qfHelpBlock">
|
||||||
|
<small id="qfHelpBlock" class="form-text text-muted">
|
||||||
|
0 means no limit
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2"></div>
|
||||||
|
<label for="idQuotaSize" class="col-sm-2 col-form-label">Quota size (bytes)</label>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<input type="number" class="form-control" id="idQuotaSize" name="quota_size" placeholder=""
|
||||||
|
value="{{.User.QuotaSize}}" min="0" aria-describedby="qsHelpBlock">
|
||||||
|
<small id="qsHelpBlock" class="form-text text-muted">
|
||||||
|
0 means no limit
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idUploadBandwidth" class="col-sm-2 col-form-label">Bandwidth UL (KB/s)</label>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<input type="number" class="form-control" id="idUploadBandwidth" name="upload_bandwidth" placeholder=""
|
||||||
|
value="{{.User.UploadBandwidth}}" min="0" aria-describedby="ulHelpBlock">
|
||||||
|
<small id="ulHelpBlock" class="form-text text-muted">
|
||||||
|
0 means no limit
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-2"></div>
|
||||||
|
<label for="idDownloadBandwidth" class="col-sm-2 col-form-label">Bandwidth DL (KB/s)</label>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<input type="number" class="form-control" id="idDownloadBandwidth" name="download_bandwidth" placeholder=""
|
||||||
|
value="{{.User.DownloadBandwidth}}" min="0" aria-describedby="dlHelpBlock">
|
||||||
|
<small id="dlHelpBlock" class="form-text text-muted">
|
||||||
|
0 means no limit
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="idMaxSessions" class="col-sm-2 col-form-label">Max sessions</label>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
<input type="number" class="form-control" id="idMaxSessions" name="max_sessions" placeholder=""
|
||||||
|
value="{{.User.MaxSessions}}" min="0" aria-describedby="sessionsHelpBlock">
|
||||||
|
<small id="sessionsHelpBlock" class="form-text text-muted">
|
||||||
|
0 means no limit
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-1"></div>
|
||||||
|
<label for="idUID" class="col-sm-1 col-form-label">UID</label>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
<input type="number" class="form-control" id="idUID" name="uid" placeholder="" value="{{.User.UID}}" min="0"
|
||||||
|
max="65535">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-1"></div>
|
||||||
|
<label for="idGID" class="col-sm-1 col-form-label">GID</label>
|
||||||
|
<div class="col-sm-2">
|
||||||
|
<input type="number" class="form-control" id="idGID" name="gid" placeholder="" value="{{.User.GID}}" min="0"
|
||||||
|
max="65535">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary float-right mt-3 mb-5 px-5 px-3">Submit</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{end}}
|
182
templates/users.html
Normal file
182
templates/users.html
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
{{template "base" .}}
|
||||||
|
|
||||||
|
{{define "title"}}{{.Title}}{{end}}
|
||||||
|
|
||||||
|
{{define "extra_css"}}
|
||||||
|
<link href="/static/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
|
||||||
|
<link href="/static/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
|
||||||
|
<link href="/static/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "page_body"}}
|
||||||
|
|
||||||
|
<div id="errorMsg" class="card mb-4 border-left-warning" style="display: none;">
|
||||||
|
<div id="errorTxt" class="card-body text-form-error"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header py-3">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">View and manage users</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-bordered" id="dataTable" width="100%" cellspacing="0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Permissions</th>
|
||||||
|
<th>Bandwidth</th>
|
||||||
|
<th>Quota</th>
|
||||||
|
<th>Other</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Users}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.ID}}</td>
|
||||||
|
<td>{{.Username}}</td>
|
||||||
|
<td>{{.GetPermissionsAsString}}</td>
|
||||||
|
<td>{{.GetBandwidthAsString}}</td>
|
||||||
|
<td>{{.GetQuotaSummary}}</td>
|
||||||
|
<td>{{.GetInfoString}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "dialog"}}
|
||||||
|
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="deleteModalLabel">
|
||||||
|
Confirmation required
|
||||||
|
</h5>
|
||||||
|
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">Do you want to delete the selected user?</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" type="button" data-dismiss="modal">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-warning" href="#" onclick="deleteAction()">
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "extra_js"}}
|
||||||
|
<script src="/static/vendor/datatables/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="/static/vendor/datatables/dataTables.bootstrap4.min.js"></script>
|
||||||
|
<script src="/static/vendor/datatables/dataTables.select.min.js"></script>
|
||||||
|
<script src="/static/vendor/datatables/select.bootstrap4.min.js"></script>
|
||||||
|
<script src="/static/vendor/datatables/dataTables.buttons.min.js"></script>
|
||||||
|
<script src="/static/vendor/datatables/buttons.bootstrap4.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
function deleteAction() {
|
||||||
|
var table = $('#dataTable').DataTable();
|
||||||
|
table.button(2).enable(false);
|
||||||
|
var userID = table.row({ selected: true }).data()[0];
|
||||||
|
var path = '{{.APIUserURL}}'.trimEnd("/") + "/" + userID;
|
||||||
|
$('#deleteModal').modal('hide');
|
||||||
|
$.ajax({
|
||||||
|
url: path,
|
||||||
|
type: 'DELETE',
|
||||||
|
dataType: 'json',
|
||||||
|
timeout: 15000,
|
||||||
|
success: function (result) {
|
||||||
|
table.button(2).enable(true);
|
||||||
|
window.location.href = '{{.UsersURL}}';
|
||||||
|
},
|
||||||
|
error: function ($xhr, textStatus, errorThrown) {
|
||||||
|
console.log("delete error")
|
||||||
|
table.button(2).enable(true);
|
||||||
|
var txt = "Unable to delete the selected user";
|
||||||
|
if ($xhr) {
|
||||||
|
var json = $xhr.responseJSON;
|
||||||
|
if (json) {
|
||||||
|
txt += ": " + json.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$('#errorTxt').text(txt);
|
||||||
|
$('#errorMsg').show();
|
||||||
|
setTimeout(function () {
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
$.fn.dataTable.ext.buttons.add = {
|
||||||
|
text: 'Add',
|
||||||
|
action: function (e, dt, node, config) {
|
||||||
|
window.location.href = '{{.UserURL}}';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.dataTable.ext.buttons.edit = {
|
||||||
|
text: 'Edit',
|
||||||
|
action: function (e, dt, node, config) {
|
||||||
|
var userID = dt.row({ selected: true }).data()[0];
|
||||||
|
var path = '{{.UserURL}}'.trimEnd("/") + "/" + userID;
|
||||||
|
window.location.href = path;
|
||||||
|
},
|
||||||
|
enabled: false
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.dataTable.ext.buttons.delete = {
|
||||||
|
text: 'Delete',
|
||||||
|
action: function (e, dt, node, config) {
|
||||||
|
/*console.log("delete clicked, num row selected: " + dt.rows({ selected: true }).count());
|
||||||
|
var data = dt.rows({ selected: true }).data();
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
console.log("selected row data: " + JSON.stringify(data[i]));
|
||||||
|
}*/
|
||||||
|
$('#deleteModal').modal('show');
|
||||||
|
},
|
||||||
|
enabled: false
|
||||||
|
};
|
||||||
|
|
||||||
|
var table = $('#dataTable').DataTable({
|
||||||
|
dom: "<'row'<'col-sm-12'B>>" +
|
||||||
|
"<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
|
||||||
|
"<'row'<'col-sm-12'tr>>" +
|
||||||
|
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||||
|
select: true,
|
||||||
|
buttons: [
|
||||||
|
'add', 'edit', 'delete'
|
||||||
|
],
|
||||||
|
"columnDefs": [
|
||||||
|
{
|
||||||
|
"targets": [0],
|
||||||
|
"visible": false,
|
||||||
|
"searchable": false
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"scrollX": false,
|
||||||
|
"order": [[1, 'asc']]
|
||||||
|
});
|
||||||
|
|
||||||
|
table.on('select deselect', function () {
|
||||||
|
var selectedRows = table.rows({ selected: true }).count();
|
||||||
|
table.button(1).enable(selectedRows == 1);
|
||||||
|
table.button(2).enable(selectedRows == 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
|
@ -2,6 +2,7 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
@ -39,6 +40,11 @@ func GetTimeAsMsSinceEpoch(t time.Time) int64 {
|
||||||
return t.UnixNano() / 1000000
|
return t.UnixNano() / 1000000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTimeFromMsecSinceEpoch return a time struct from a unix timestamp with millisecond precision
|
||||||
|
func GetTimeFromMsecSinceEpoch(msec int64) time.Time {
|
||||||
|
return time.Unix(0, msec*1000000)
|
||||||
|
}
|
||||||
|
|
||||||
// ScanDirContents returns the number of files contained in a directory, their size and a slice with the file paths
|
// ScanDirContents returns the number of files contained in a directory, their size and a slice with the file paths
|
||||||
func ScanDirContents(path string) (int, int64, []string, error) {
|
func ScanDirContents(path string) (int, int64, []string, error) {
|
||||||
var numFiles int
|
var numFiles int
|
||||||
|
@ -86,3 +92,44 @@ func SetPathPermissions(path string, uid int, gid int) {
|
||||||
func GetAppVersion() VersionInfo {
|
func GetAppVersion() VersionInfo {
|
||||||
return versionInfo
|
return versionInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDurationAsString returns a string representation for a time.Duration
|
||||||
|
func GetDurationAsString(d time.Duration) string {
|
||||||
|
d = d.Round(time.Second)
|
||||||
|
h := d / time.Hour
|
||||||
|
d -= h * time.Hour
|
||||||
|
m := d / time.Minute
|
||||||
|
d -= m * time.Minute
|
||||||
|
s := d / time.Second
|
||||||
|
if h > 0 {
|
||||||
|
return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%02d:%02d", m, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByteCountSI returns humanized size in SI (decimal) format
|
||||||
|
func ByteCountSI(b int64) string {
|
||||||
|
return byteCount(b, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByteCountIEC returns humanized size in IEC (binary) format
|
||||||
|
func ByteCountIEC(b int64) string {
|
||||||
|
return byteCount(b, 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
func byteCount(b int64, unit int64) string {
|
||||||
|
if b < unit {
|
||||||
|
return fmt.Sprintf("%d B", b)
|
||||||
|
}
|
||||||
|
div, exp := unit, 0
|
||||||
|
for n := b / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
if unit == 1000 {
|
||||||
|
return fmt.Sprintf("%.1f %cB",
|
||||||
|
float64(b)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %ciB",
|
||||||
|
float64(b)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue