Compare commits

...

17 commits
main ... v2.5.4

Author SHA1 Message Date
Nicola Murino
cc381443be
set version to 2.5.4
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-07-14 20:35:45 +02:00
Nicola Murino
89a251d640
update pgx to the latest commit
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-07-09 11:23:26 +02:00
Nicola Murino
dbbae3129d
update deps
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-07-08 17:21:55 +02:00
Nicola Murino
c457538280
file patterns: fix denied except rules
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-07-08 17:09:44 +02:00
Nicola Murino
7f65aa1fa4
set version to 2.5.3-dev
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-07-04 19:43:00 +02:00
Nicola Murino
abac3cfc8d
revert pgx to an older version
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-07-04 13:41:32 +02:00
Nicola Murino
a805a930e8
set version to 2.5.3
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-29 12:21:13 +02:00
Nicola Murino
de72495092
Windows setup: add PrepareToInstall event function
so the service is stopped before the installation starts and
we avoid the force close app warning

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-29 12:15:38 +02:00
Nicola Murino
7c845f07d5
config: fix loading commands args from env vars
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-25 21:32:37 +02:00
Nicola Murino
b9ace46180
add auth plugin
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-25 18:58:13 +02:00
Nicola Murino
e446e3392d
check second factor after plugin authentication
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-25 07:18:42 +02:00
Nicola Murino
a503feaab6
set version to 2.5.2
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-17 18:33:56 +02:00
Nicola Murino
cba894987c
WebClient: show user quota
Also remove per-source data transfer limits. This was an
oversight

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-16 21:31:15 +02:00
Nicola Murino
1d120bdd26
WebAdmin: don't show hidden deny policy for allowed patterns
The deny policy only applies to denied patterns, showing an allowed
pattern as hidden will confuse users

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-14 19:01:22 +02:00
Nicola Murino
7245710b31
CI: fix MariaDB initialization
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-12 20:04:48 +02:00
Nicola Murino
3a3df5670d
WebAdmin: relax key prefix validation
try to automatically fix leading and trailing slashes

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-12 19:15:02 +02:00
Nicola Murino
97bbf37af4
branch 2.5.x
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2023-06-09 19:47:17 +02:00
38 changed files with 1555 additions and 1133 deletions

View file

@ -2,7 +2,7 @@ name: CI
on:
push:
branches: [main]
branches: [2.5.x]
pull_request:
jobs:
@ -268,7 +268,7 @@ jobs:
MYSQL_USER: sftpgo
MYSQL_PASSWORD: sftpgo
options: >-
--health-cmd "mysqladmin status -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD"
--health-cmd "mariadb-admin status -h 127.0.0.1 -P 3306 -u root -p$MYSQL_ROOT_PASSWORD"
--health-interval 10s
--health-timeout 5s
--health-retries 6

View file

@ -5,7 +5,7 @@ on:
# - cron: '0 4 * * *' # everyday at 4:00 AM UTC
push:
branches:
- main
- 2.5.x
tags:
- v*
pull_request:

View file

@ -5,7 +5,7 @@ on:
tags: 'v*'
env:
GO_VERSION: 1.20.5
GO_VERSION: 1.20.6
jobs:
prepare-sources-with-deps:

View file

@ -22,7 +22,7 @@ I'd like to make SFTPGo into a sustainable long term project and would not like
If you use SFTPGo, it is in your best interest to ensure that the project you rely on stays healthy and well maintained.
This can only happen with your donations and [sponsorships](https://github.com/sponsors/drakkan) :heart:
You can also purchase support plans from the [SFTPGo website](https://sftpgo.com/#pricing).
You can also purchase, using many payment methods, support plans from the [SFTPGo website](https://sftpgo.com/#pricing).
With sponsorships/donations or support plans we establish a channel for reciprocal access, ensuring better outcomes for both you and the project.
@ -67,6 +67,7 @@ If you report an invalid issue or ask for step-by-step support, your issue will
- Partial authentication. You can configure multi-step authentication requiring, for example, the user password after successful public key authentication.
- Per-user authentication methods.
- [Two-factor authentication](./docs/howto/two-factor-authentication.md) based on time-based one time passwords (RFC 6238) which works with Authy, Google Authenticator, Microsoft Authenticator and other compatible apps.
- LDAP/Active Directory authentication using a [plugin](https://github.com/sftpgo/sftpgo-plugin-auth).
- Simplified user administrations using [groups](./docs/groups.md).
- [Roles](./docs/roles.md) allow to create limited administrators who can only create and manage users with their role.
- Custom authentication via [external programs/HTTP API](./docs/external-auth.md).

View file

@ -4,11 +4,11 @@ SFTPGo provides an official Docker image, it is available on both [Docker Hub](h
## Supported tags and respective Dockerfile links
- [v2.5.1, v2.5, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile)
- [v2.5.1-plugins, v2.5-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile)
- [v2.5.1-alpine, v2.5-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile.alpine)
- [v2.5.1-slim, v2.5-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile)
- [v2.5.1-alpine-slim, v2.5-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.5.1/Dockerfile.alpine)
- [v2.5.4, v2.5, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.5.4/Dockerfile)
- [v2.5.4-plugins, v2.5-plugins, v2-plugins, plugins](https://github.com/drakkan/sftpgo/blob/v2.5.4/Dockerfile)
- [v2.5.4-alpine, v2.5-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.5.4/Dockerfile.alpine)
- [v2.5.4-slim, v2.5-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.5.4/Dockerfile)
- [v2.5.4-alpine-slim, v2.5-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.5.4/Dockerfile.alpine)
- [edge](../Dockerfile)
- [edge-plugins](../Dockerfile)
- [edge-alpine](../Dockerfile.alpine)

View file

@ -17,7 +17,7 @@ esac
echo "download plugins for arch ${SUFFIX}"
for PLUGIN in geoipfilter kms pubsub eventstore eventsearch metadata
for PLUGIN in geoipfilter kms pubsub eventstore eventsearch metadata auth
do
echo "download plugin from https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}"
curl -L "https://github.com/sftpgo/sftpgo-plugin-${PLUGIN}/releases/latest/download/sftpgo-plugin-${PLUGIN}-linux-${SUFFIX}" --output "/usr/local/bin/sftpgo-plugin-${PLUGIN}"

120
go.mod
View file

@ -3,32 +3,32 @@ module github.com/drakkan/sftpgo/v2
go 1.20
require (
cloud.google.com/go/storage v1.30.1
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.0.0
cloud.google.com/go/storage v1.31.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
github.com/aws/aws-sdk-go-v2 v1.18.0
github.com/aws/aws-sdk-go-v2/config v1.18.25
github.com/aws/aws-sdk-go-v2/credentials v1.13.24
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.19.8
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0
github.com/aws/aws-sdk-go-v2 v1.19.0
github.com/aws/aws-sdk-go-v2/config v1.18.28
github.com/aws/aws-sdk-go-v2/credentials v1.13.27
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.72
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.14
github.com/aws/aws-sdk-go-v2/service/s3 v1.37.0
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.19.11
github.com/aws/aws-sdk-go-v2/service/sts v1.19.3
github.com/bmatcuk/doublestar/v4 v4.6.0
github.com/cockroachdb/cockroach-go/v2 v2.3.4
github.com/cockroachdb/cockroach-go/v2 v2.3.5
github.com/coreos/go-oidc/v3 v3.6.0
github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
github.com/fclairamb/ftpserverlib v0.21.0
github.com/fclairamb/go-log v0.4.1
github.com/go-acme/lego/v4 v4.12.1
github.com/go-chi/chi/v5 v5.0.9-0.20230502103705-7f280968675b
github.com/go-chi/jwtauth/v5 v5.1.0
github.com/go-chi/render v1.0.2
github.com/go-acme/lego/v4 v4.12.3
github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/jwtauth/v5 v5.1.1
github.com/go-chi/render v1.0.3
github.com/go-sql-driver/mysql v1.7.1
github.com/golang/mock v1.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
@ -36,66 +36,66 @@ require (
github.com/hashicorp/go-hclog v1.5.0
github.com/hashicorp/go-plugin v1.4.10
github.com/hashicorp/go-retryablehttp v0.7.4
github.com/jackc/pgx/v5 v5.3.2-0.20230603125928-d9560c78b8e6
github.com/jackc/pgx/v5 v5.4.2
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/klauspost/compress v1.16.5
github.com/lestrrat-go/jwx/v2 v2.0.9
github.com/klauspost/compress v1.16.7
github.com/lestrrat-go/jwx/v2 v2.0.11
github.com/lithammer/shortuuid/v3 v3.0.7
github.com/mattn/go-sqlite3 v1.14.17
github.com/mhale/smtpd v0.8.0
github.com/minio/sio v0.3.1
github.com/otiai10/copy v1.11.0
github.com/otiai10/copy v1.12.0
github.com/pires/go-proxyproto v0.7.0
github.com/pkg/sftp v1.13.6-0.20230213180117-971c283182b6
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.15.1
github.com/prometheus/client_golang v1.16.0
github.com/robfig/cron/v3 v3.0.1
github.com/rs/cors v1.9.0
github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.29.1
github.com/sftpgo/sdk v0.1.5-0.20230524172149-afb96ebee860
github.com/shirou/gopsutil/v3 v3.23.5
github.com/sftpgo/sdk v0.1.5
github.com/shirou/gopsutil/v3 v3.23.6
github.com/spf13/afero v1.9.5
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.4
github.com/studio-b12/gowebdav v0.0.0-20230203202212-3282f94193f2
github.com/studio-b12/gowebdav v0.9.0
github.com/subosito/gotenv v1.4.2
github.com/unrolled/secure v1.13.0
github.com/wagslane/go-password-validator v0.3.0
github.com/wneessen/go-mail v0.3.10-0.20230531074101-4100fef083df
github.com/wneessen/go-mail v0.4.0
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
go.etcd.io/bbolt v1.3.7
go.uber.org/automaxprocs v1.5.2
gocloud.dev v0.29.0
golang.org/x/crypto v0.9.0
golang.org/x/net v0.10.0
golang.org/x/oauth2 v0.8.0
golang.org/x/sys v0.8.0
golang.org/x/term v0.8.0
gocloud.dev v0.30.0
golang.org/x/crypto v0.11.0
golang.org/x/net v0.12.0
golang.org/x/oauth2 v0.10.0
golang.org/x/sys v0.10.0
golang.org/x/term v0.10.0
golang.org/x/time v0.3.0
google.golang.org/api v0.125.0
google.golang.org/api v0.131.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
cloud.google.com/go v0.110.2 // indirect
cloud.google.com/go/compute v1.20.0 // indirect
cloud.google.com/go v0.110.6 // indirect
cloud.google.com/go/compute v1.21.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.0 // indirect
cloud.google.com/go/iam v1.1.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.27 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.30 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
@ -114,8 +114,8 @@ require (
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.4 // indirect
github.com/googleapis/gax-go/v2 v2.10.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
@ -135,18 +135,19 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/miekg/dns v1.1.54 // indirect
github.com/miekg/dns v1.1.55 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/prometheus/procfs v0.11.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
@ -155,22 +156,23 @@ require (
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.9.3 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/tools v0.11.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/grpc v1.56.2 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace (
github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20230714144823-d8aff325a796
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20230608154636-e9d673c2a1a8
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20230614155948-29e7be6c0fab
)

836
go.sum

File diff suppressed because it is too large Load diff

View file

@ -1316,8 +1316,7 @@ func (c *BaseConnection) GetTransferQuota() dataprovider.TransferQuota {
}
func (c *BaseConnection) checkUserQuota() (dataprovider.TransferQuota, int, int64) {
clientIP := c.GetRemoteIP()
ul, dl, total := c.User.GetDataTransferLimits(clientIP)
ul, dl, total := c.User.GetDataTransferLimits()
result := dataprovider.TransferQuota{
ULSize: ul,
DLSize: dl,

View file

@ -28,6 +28,7 @@ import (
"github.com/rs/xid"
"github.com/sftpgo/sdk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/kms"
@ -647,7 +648,7 @@ func TestFsFileCopier(t *testing.T) {
assert.True(t, ok)
}
func TestFilterListDirs(t *testing.T) {
func TestFilePatterns(t *testing.T) {
filters := dataprovider.UserFilters{
BaseUserFilters: sdk.BaseUserFilters{
FilePatterns: []sdk.PatternsFilter{
@ -661,6 +662,16 @@ func TestFilterListDirs(t *testing.T) {
DenyPolicy: sdk.DenyPolicyHide,
AllowedPatterns: []string{"*.jpg"},
},
{
Path: "/dir3",
DenyPolicy: sdk.DenyPolicyDefault,
DeniedPatterns: []string{"*.jpg"},
},
{
Path: "/dir4",
DenyPolicy: sdk.DenyPolicyHide,
DeniedPatterns: []string{"*"},
},
},
},
}
@ -693,13 +704,90 @@ func TestFilterListDirs(t *testing.T) {
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
// dirContents are modified in place, we need to redefine them each time
filtered := user.FilterListDir(dirContents, "/dir1")
assert.Len(t, filtered, 5)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir1/vdir1")
assert.Len(t, filtered, 2)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir2/vdir2")
require.Len(t, filtered, 1)
assert.Equal(t, "file1.jpg", filtered[0].Name())
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir2/vdir2/sub")
require.Len(t, filtered, 1)
assert.Equal(t, "file1.jpg", filtered[0].Name())
res, _ := user.IsFileAllowed("/dir1/vdir1/file.txt")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir1/vdir1/sub/file.txt")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir1/vdir1/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir1/vdir1/sub/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/dir1/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/dir1/sub/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir4/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir4/dir1/sub/file.jpg")
assert.False(t, res)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir4")
require.Len(t, filtered, 0)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir4/vdir2/sub")
require.Len(t, filtered, 0)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir2")
assert.Len(t, filtered, 2)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir4")
assert.Len(t, filtered, 0)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir4/sub")
assert.Len(t, filtered, 0)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("vdir3.jpg", false, 123, time.Now(), false),
@ -708,11 +796,6 @@ func TestFilterListDirs(t *testing.T) {
filtered = user.FilterListDir(dirContents, "/dir1")
assert.Len(t, filtered, 5)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("vdir3.jpg", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir2")
if assert.Len(t, filtered, 1) {
assert.True(t, filtered[0].IsDir())
@ -799,7 +882,155 @@ func TestFilterListDirs(t *testing.T) {
dirContents = append(dirContents, vfs.NewFileInfo("file.jpg", false, 123, time.Now(), false))
filtered = user.FilterListDir(dirContents, "/dir3")
if assert.Len(t, filtered, 1) {
assert.Equal(t, "ic35", filtered[0].Name())
require.Len(t, filtered, 1)
assert.Equal(t, "ic35", filtered[0].Name())
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic36")
require.Len(t, filtered, 0)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35")
require.Len(t, filtered, 3)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub")
require.Len(t, filtered, 3)
res, _ = user.IsFileAllowed("/dir3/file.txt")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35a")
assert.False(t, res)
res, policy := user.IsFileAllowed("/dir3/ic35a/file")
assert.False(t, res)
assert.Equal(t, sdk.DenyPolicyHide, policy)
res, _ = user.IsFileAllowed("/dir3/ic35")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub/file.txt")
assert.True(t, res)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub")
require.Len(t, filtered, 3)
user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
Path: "/dir3/ic35/sub1",
AllowedPatterns: []string{"*.jpg"},
DenyPolicy: sdk.DenyPolicyDefault,
})
user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
Path: "/dir3/ic35/sub2",
DeniedPatterns: []string{"*.jpg"},
DenyPolicy: sdk.DenyPolicyHide,
})
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub1")
require.Len(t, filtered, 3)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub2")
require.Len(t, filtered, 2)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35/sub2/sub1")
require.Len(t, filtered, 2)
res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub/dir/file.txt")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub/dir/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub1/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub1/file.txt")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub1/sub/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub1/sub2/file.txt")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.txt")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub1/file.txt")
assert.True(t, res)
user.Filters.FilePatterns = append(user.Filters.FilePatterns, sdk.PatternsFilter{
Path: "/dir3/ic35",
DeniedPatterns: []string{"*.txt"},
DenyPolicy: sdk.DenyPolicyHide,
})
res, _ = user.IsFileAllowed("/dir3/ic35/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/file.txt")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/adir/sub/file.jpg")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/adir/file.txt")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/file.txt")
assert.True(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub/file.jpg")
assert.False(t, res)
res, _ = user.IsFileAllowed("/dir3/ic35/sub2/sub1/file.txt")
assert.True(t, res)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35")
require.Len(t, filtered, 1)
dirContents = []os.FileInfo{
vfs.NewFileInfo("file1.jpg", false, 123, time.Now(), false),
vfs.NewFileInfo("file1.txt", false, 123, time.Now(), false),
vfs.NewFileInfo("file2.txt", false, 123, time.Now(), false),
}
filtered = user.FilterListDir(dirContents, "/dir3/ic35/abc")
require.Len(t, filtered, 1)
}

View file

@ -1525,7 +1525,6 @@ func getUserForEventAction(user dataprovider.User) (dataprovider.User, error) {
user.Filters.DisableFsChecks = false
user.Filters.FilePatterns = nil
user.Filters.BandwidthLimits = nil
user.Filters.DataTransferLimits = nil
for k := range user.Permissions {
user.Permissions[k] = []string{dataprovider.PermAny}
}

View file

@ -1548,12 +1548,6 @@ func TestVirtualFoldersQuotaRenameOverwrite(t *testing.T) {
func TestQuotaRenameOverwrite(t *testing.T) {
u := getTestUser()
u.QuotaFiles = 100
u.Filters.DataTransferLimits = []sdk.DataTransferLimit{
{
Sources: []string{"10.8.0.0/8"},
TotalDataTransfer: 1,
},
}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
conn, client, err := getSftpClient(user)

View file

@ -332,41 +332,22 @@ func TestFTPMode(t *testing.T) {
func TestTransferQuota(t *testing.T) {
user := dataprovider.User{
BaseUser: sdk.BaseUser{
TotalDataTransfer: -1,
UploadDataTransfer: -1,
DownloadDataTransfer: -1,
TotalDataTransfer: 3,
UploadDataTransfer: 2,
DownloadDataTransfer: 1,
},
}
user.Filters.DataTransferLimits = []sdk.DataTransferLimit{
{
Sources: []string{"127.0.0.1/32", "192.168.1.0/24"},
TotalDataTransfer: 100,
UploadDataTransfer: 0,
DownloadDataTransfer: 0,
},
{
Sources: []string{"172.16.0.0/24"},
TotalDataTransfer: 0,
UploadDataTransfer: 120,
DownloadDataTransfer: 150,
},
}
ul, dl, total := user.GetDataTransferLimits("127.0.1.1")
ul, dl, total := user.GetDataTransferLimits()
assert.Equal(t, int64(2*1048576), ul)
assert.Equal(t, int64(1*1048576), dl)
assert.Equal(t, int64(3*1048576), total)
user.TotalDataTransfer = -1
user.UploadDataTransfer = -1
user.DownloadDataTransfer = -1
ul, dl, total = user.GetDataTransferLimits()
assert.Equal(t, int64(0), ul)
assert.Equal(t, int64(0), dl)
assert.Equal(t, int64(0), total)
ul, dl, total = user.GetDataTransferLimits("127.0.0.1")
assert.Equal(t, int64(0), ul)
assert.Equal(t, int64(0), dl)
assert.Equal(t, int64(100*1048576), total)
ul, dl, total = user.GetDataTransferLimits("192.168.1.4")
assert.Equal(t, int64(0), ul)
assert.Equal(t, int64(0), dl)
assert.Equal(t, int64(100*1048576), total)
ul, dl, total = user.GetDataTransferLimits("172.16.0.2")
assert.Equal(t, int64(120*1048576), ul)
assert.Equal(t, int64(150*1048576), dl)
assert.Equal(t, int64(0), total)
transferQuota := dataprovider.TransferQuota{}
assert.True(t, transferQuota.HasDownloadSpace())
assert.True(t, transferQuota.HasUploadSpace())

View file

@ -61,7 +61,7 @@ type baseTransferChecker struct {
func (t *baseTransferChecker) isDataTransferExceeded(user dataprovider.User, transfer dataprovider.ActiveTransfer, ulSize,
dlSize int64,
) bool {
ulQuota, dlQuota, totalQuota := user.GetDataTransferLimits(transfer.IP)
ulQuota, dlQuota, totalQuota := user.GetDataTransferLimits()
if totalQuota > 0 {
allowedSize := totalQuota - (user.UsedUploadDataTransfer + user.UsedDownloadDataTransfer)
if ulSize+dlSize > allowedSize {

View file

@ -622,17 +622,6 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
QuotaSize: 100,
},
},
Filters: dataprovider.UserFilters{
BaseUserFilters: sdk.BaseUserFilters{
DataTransferLimits: []sdk.DataTransferLimit{
{
Sources: []string{"172.16.0.0/16"},
UploadDataTransfer: 50,
DownloadDataTransfer: 80,
},
},
},
},
}
err = dataprovider.AddUser(&user, "", "", "")
assert.NoError(t, err)
@ -660,14 +649,10 @@ func TestGetUsersForQuotaCheck(t *testing.T) {
assert.Len(t, user.VirtualFolders, 0, user.Username)
}
}
ul, dl, total := user.GetDataTransferLimits("127.1.1.1")
ul, dl, total := user.GetDataTransferLimits()
assert.Equal(t, int64(0), ul)
assert.Equal(t, int64(0), dl)
assert.Equal(t, int64(0), total)
ul, dl, total = user.GetDataTransferLimits("172.16.2.3")
assert.Equal(t, int64(50*1024*1024), ul)
assert.Equal(t, int64(80*1024*1024), dl)
assert.Equal(t, int64(0), total)
}
for i := 0; i < 40; i++ {

View file

@ -1961,6 +1961,11 @@ func getCommandConfigsFromEnv(idx int) {
cfg.Env = env
}
args, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_COMMAND__COMMANDS__%v__ARGS", idx))
if ok {
cfg.Args = args
}
if cfg.Path != "" {
if len(globalConf.CommandConfig.Commands) > idx {
globalConf.CommandConfig.Commands[idx] = cfg

View file

@ -924,13 +924,17 @@ func TestCommandsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__PATH", "cmd2")
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__TIMEOUT", "20")
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__ENV", "e=f")
os.Setenv("SFTPGO_COMMAND__COMMANDS__1__ARGS", "arg1, arg2")
t.Cleanup(func() {
os.Unsetenv("SFTPGO_COMMAND__TIMEOUT")
os.Unsetenv("SFTPGO_COMMAND__ENV")
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__PATH")
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__TIMEOUT")
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__0__ENV")
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__1__PATH")
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__1__TIMEOUT")
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__1__ENV")
os.Unsetenv("SFTPGO_COMMAND__COMMANDS__1__ARGS")
})
err = config.LoadConfig(configDir, confName)
@ -945,6 +949,7 @@ func TestCommandsFromEnv(t *testing.T) {
require.Equal(t, "cmd2", commandConfig.Commands[1].Path)
require.Equal(t, 20, commandConfig.Commands[1].Timeout)
require.Equal(t, []string{"e=f"}, commandConfig.Commands[1].Env)
require.Equal(t, []string{"arg1", "arg2"}, commandConfig.Commands[1].Args)
err = os.Remove(configFilePath)
assert.NoError(t, err)

View file

@ -2585,18 +2585,6 @@ func copyBaseUserFilters(in sdk.BaseUserFilters) sdk.BaseUserFilters {
copy(bwLimit.Sources, limit.Sources)
filters.BandwidthLimits = append(filters.BandwidthLimits, bwLimit)
}
filters.DataTransferLimits = make([]sdk.DataTransferLimit, 0, len(in.DataTransferLimits))
for _, limit := range in.DataTransferLimits {
dtLimit := sdk.DataTransferLimit{
UploadDataTransfer: limit.UploadDataTransfer,
DownloadDataTransfer: limit.DownloadDataTransfer,
TotalDataTransfer: limit.TotalDataTransfer,
Sources: make([]string, 0, len(limit.Sources)),
}
dtLimit.Sources = make([]string, len(limit.Sources))
copy(dtLimit.Sources, limit.Sources)
filters.DataTransferLimits = append(filters.DataTransferLimits, dtLimit)
}
return filters
}
@ -2943,26 +2931,6 @@ func validateBandwidthLimitsFilter(filters *sdk.BaseUserFilters) error {
return nil
}
func validateTransferLimitsFilter(filters *sdk.BaseUserFilters) error {
for idx, limit := range filters.DataTransferLimits {
filters.DataTransferLimits[idx].Sources = util.RemoveDuplicates(limit.Sources, false)
if len(limit.Sources) == 0 {
return util.NewValidationError("no data transfer limit source specified")
}
for _, source := range limit.Sources {
_, _, err := net.ParseCIDR(source)
if err != nil {
return util.NewValidationError(fmt.Sprintf("could not parse data transfer limit source %q: %v", source, err))
}
}
if limit.TotalDataTransfer > 0 {
filters.DataTransferLimits[idx].UploadDataTransfer = 0
filters.DataTransferLimits[idx].DownloadDataTransfer = 0
}
}
return nil
}
func updateFiltersValues(filters *sdk.BaseUserFilters) {
if filters.StartDirectory != "" {
filters.StartDirectory = util.CleanPath(filters.StartDirectory)
@ -2998,9 +2966,6 @@ func validateBaseFilters(filters *sdk.BaseUserFilters) error {
if err := validateBandwidthLimitsFilter(filters); err != nil {
return err
}
if err := validateTransferLimitsFilter(filters); err != nil {
return err
}
if len(filters.DeniedLoginMethods) >= len(ValidLoginMethods) {
return util.NewValidationError("invalid denied_login_methods")
}
@ -3525,7 +3490,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
return 0, err
}
if len(answers) != 1 {
return 0, fmt.Errorf("unexpected number of answers: %v", len(answers))
return 0, fmt.Errorf("unexpected number of answers: %d", len(answers))
}
err = user.LoadAndApplyGroupSettings()
if err != nil {
@ -3535,16 +3500,20 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
if err != nil {
return 0, err
}
return checkKeyboardInteractiveSecondFactor(user, client, protocol)
}
func checkKeyboardInteractiveSecondFactor(user *User, client ssh.KeyboardInteractiveChallenge, protocol string) (int, error) {
if !user.Filters.TOTPConfig.Enabled || !util.Contains(user.Filters.TOTPConfig.Protocols, protocolSSH) {
return 1, nil
}
err = user.Filters.TOTPConfig.Secret.TryDecrypt()
err := user.Filters.TOTPConfig.Secret.TryDecrypt()
if err != nil {
providerLog(logger.LevelError, "unable to decrypt TOTP secret for user %q, protocol %v, err: %v",
user.Username, protocol, err)
return 0, err
}
answers, err = client("", "", []string{"Authentication code: "}, []bool{false})
answers, err := client("", "", []string{"Authentication code: "}, []bool{false})
if err != nil {
return 0, err
}
@ -3777,6 +3746,9 @@ func doKeyboardInteractiveAuth(user *User, authHook string, client ssh.KeyboardI
var err error
if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {
authResult, err = executeKeyboardInteractivePlugin(user, client, ip, protocol)
if authResult == 1 && err == nil {
authResult, err = checkKeyboardInteractiveSecondFactor(user, client, protocol)
}
} else if authHook != "" {
if strings.HasPrefix(authHook, "http") {
authResult, err = executeKeyboardInteractiveHTTPHook(user, authHook, client, ip, protocol)

View file

@ -981,9 +981,14 @@ func (u *User) getPatternsFilterForPath(virtualPath string) sdk.PatternsFilter {
return filter
}
dirsForPath := util.GetDirsForVirtualPath(virtualPath)
for _, dir := range dirsForPath {
for idx, dir := range dirsForPath {
for _, f := range u.Filters.FilePatterns {
if f.Path == dir {
if idx > 0 && len(f.AllowedPatterns) > 0 && len(f.DeniedPatterns) > 0 && f.DeniedPatterns[0] == "*" {
if f.CheckAllowed(path.Base(dirsForPath[idx-1])) {
return filter
}
}
filter = f
break
}
@ -1004,7 +1009,7 @@ func (u *User) isDirHidden(virtualPath string) bool {
return false
}
filter := u.getPatternsFilterForPath(dirPath)
if filter.DenyPolicy == sdk.DenyPolicyHide {
if filter.DenyPolicy == sdk.DenyPolicyHide && filter.Path != dirPath {
if !filter.CheckAllowed(path.Base(dirPath)) {
return true
}
@ -1289,39 +1294,12 @@ func (u *User) HasQuotaRestrictions() bool {
// HasTransferQuotaRestrictions returns true if there are any data transfer restrictions
func (u *User) HasTransferQuotaRestrictions() bool {
if len(u.Filters.DataTransferLimits) > 0 {
return true
}
return u.UploadDataTransfer > 0 || u.TotalDataTransfer > 0 || u.DownloadDataTransfer > 0
}
// GetDataTransferLimits returns upload, download and total data transfer limits
func (u *User) GetDataTransferLimits(clientIP string) (int64, int64, int64) {
func (u *User) GetDataTransferLimits() (int64, int64, int64) {
var total, ul, dl int64
if len(u.Filters.DataTransferLimits) > 0 {
ip := net.ParseIP(clientIP)
if ip != nil {
for _, limit := range u.Filters.DataTransferLimits {
for _, source := range limit.Sources {
_, ipNet, err := net.ParseCIDR(source)
if err == nil {
if ipNet.Contains(ip) {
if limit.TotalDataTransfer > 0 {
total = limit.TotalDataTransfer * 1048576
}
if limit.DownloadDataTransfer > 0 {
dl = limit.DownloadDataTransfer * 1048576
}
if limit.UploadDataTransfer > 0 {
ul = limit.UploadDataTransfer * 1048576
}
return ul, dl, total
}
}
}
}
}
}
if u.TotalDataTransfer > 0 {
total = u.TotalDataTransfer * 1048576
}
@ -1825,7 +1803,6 @@ func (u *User) mergeAdditiveProperties(group *Group, groupType int, replacer *st
u.mergePermissions(group, groupType, replacer)
u.mergeFilePatterns(group, groupType, replacer)
u.Filters.BandwidthLimits = append(u.Filters.BandwidthLimits, group.UserSettings.Filters.BandwidthLimits...)
u.Filters.DataTransferLimits = append(u.Filters.DataTransferLimits, group.UserSettings.Filters.DataTransferLimits...)
u.Filters.AllowedIP = append(u.Filters.AllowedIP, group.UserSettings.Filters.AllowedIP...)
u.Filters.DeniedIP = append(u.Filters.DeniedIP, group.UserSettings.Filters.DeniedIP...)
u.Filters.DeniedLoginMethods = append(u.Filters.DeniedLoginMethods, group.UserSettings.Filters.DeniedLoginMethods...)

View file

@ -170,7 +170,7 @@ func searchFsEvents(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Content-Type", "application/json")
w.Write(data) //nolint:errcheck
}
@ -205,7 +205,7 @@ func searchProviderEvents(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Content-Type", "application/json")
w.Write(data) //nolint:errcheck
}
@ -238,7 +238,7 @@ func searchLogEvents(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Content-Type", "application/json")
w.Write(data) //nolint:errcheck
}

View file

@ -2627,103 +2627,6 @@ func TestEventRuleValidation(t *testing.T) {
assert.Contains(t, string(resp), "invalid Identity Provider login event")
}
func TestUserTransferLimits(t *testing.T) {
u := getTestUser()
u.TotalDataTransfer = 100
u.Filters.DataTransferLimits = []sdk.DataTransferLimit{
{
Sources: nil,
},
}
_, resp, err := httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err, string(resp))
assert.Contains(t, string(resp), "Validation error: no data transfer limit source specified")
u.Filters.DataTransferLimits = []sdk.DataTransferLimit{
{
Sources: []string{"a"},
},
}
_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)
assert.NoError(t, err, string(resp))
assert.Contains(t, string(resp), "Validation error: could not parse data transfer limit source")
u.Filters.DataTransferLimits = []sdk.DataTransferLimit{
{
Sources: []string{"127.0.0.1/32"},
UploadDataTransfer: 120,
DownloadDataTransfer: 140,
},
{
Sources: []string{"192.168.0.0/24", "192.168.1.0/24"},
TotalDataTransfer: 400,
},
{
Sources: []string{"10.0.0.0/8"},
},
}
user, resp, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err, string(resp))
assert.Len(t, user.Filters.DataTransferLimits, 3)
assert.Equal(t, u.Filters.DataTransferLimits, user.Filters.DataTransferLimits)
up, down, total := user.GetDataTransferLimits("1.1.1.1")
assert.Equal(t, user.TotalDataTransfer*1024*1024, total)
assert.Equal(t, user.UploadDataTransfer*1024*1024, up)
assert.Equal(t, user.DownloadDataTransfer*1024*1024, down)
up, down, total = user.GetDataTransferLimits("127.0.0.1")
assert.Equal(t, user.Filters.DataTransferLimits[0].TotalDataTransfer*1024*1024, total)
assert.Equal(t, user.Filters.DataTransferLimits[0].UploadDataTransfer*1024*1024, up)
assert.Equal(t, user.Filters.DataTransferLimits[0].DownloadDataTransfer*1024*1024, down)
up, down, total = user.GetDataTransferLimits("192.168.1.6")
assert.Equal(t, user.Filters.DataTransferLimits[1].TotalDataTransfer*1024*1024, total)
assert.Equal(t, user.Filters.DataTransferLimits[1].UploadDataTransfer*1024*1024, up)
assert.Equal(t, user.Filters.DataTransferLimits[1].DownloadDataTransfer*1024*1024, down)
up, down, total = user.GetDataTransferLimits("10.1.2.3")
assert.Equal(t, user.Filters.DataTransferLimits[2].TotalDataTransfer*1024*1024, total)
assert.Equal(t, user.Filters.DataTransferLimits[2].UploadDataTransfer*1024*1024, up)
assert.Equal(t, user.Filters.DataTransferLimits[2].DownloadDataTransfer*1024*1024, down)
connID := xid.New().String()
localAddr := "::1"
conn := common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "1.1.1.2", user)
transferQuota := conn.GetTransferQuota()
assert.Equal(t, user.TotalDataTransfer*1024*1024, transferQuota.AllowedTotalSize)
assert.Equal(t, user.UploadDataTransfer*1024*1024, transferQuota.AllowedULSize)
assert.Equal(t, user.DownloadDataTransfer*1024*1024, transferQuota.AllowedDLSize)
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "127.0.0.1", user)
transferQuota = conn.GetTransferQuota()
assert.Equal(t, user.Filters.DataTransferLimits[0].TotalDataTransfer*1024*1024, transferQuota.AllowedTotalSize)
assert.Equal(t, user.Filters.DataTransferLimits[0].UploadDataTransfer*1024*1024, transferQuota.AllowedULSize)
assert.Equal(t, user.Filters.DataTransferLimits[0].DownloadDataTransfer*1024*1024, transferQuota.AllowedDLSize)
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "192.168.1.5", user)
transferQuota = conn.GetTransferQuota()
assert.Equal(t, user.Filters.DataTransferLimits[1].TotalDataTransfer*1024*1024, transferQuota.AllowedTotalSize)
assert.Equal(t, user.Filters.DataTransferLimits[1].UploadDataTransfer*1024*1024, transferQuota.AllowedULSize)
assert.Equal(t, user.Filters.DataTransferLimits[1].DownloadDataTransfer*1024*1024, transferQuota.AllowedDLSize)
u.UsedDownloadDataTransfer = 10 * 1024 * 1024
u.UsedUploadDataTransfer = 5 * 1024 * 1024
_, err = httpdtest.UpdateTransferQuotaUsage(u, "", http.StatusOK)
assert.NoError(t, err)
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "192.168.1.6", user)
transferQuota = conn.GetTransferQuota()
assert.Equal(t, (user.Filters.DataTransferLimits[1].TotalDataTransfer-15)*1024*1024, transferQuota.AllowedTotalSize)
assert.Equal(t, user.Filters.DataTransferLimits[1].UploadDataTransfer*1024*1024, transferQuota.AllowedULSize)
assert.Equal(t, user.Filters.DataTransferLimits[1].DownloadDataTransfer*1024*1024, transferQuota.AllowedDLSize)
conn = common.NewBaseConnection(connID, common.ProtocolHTTP, localAddr, "10.8.3.4", user)
transferQuota = conn.GetTransferQuota()
assert.Equal(t, int64(0), transferQuota.AllowedTotalSize)
assert.Equal(t, int64(0), transferQuota.AllowedULSize)
assert.Equal(t, int64(0), transferQuota.AllowedDLSize)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
}
func TestUserBandwidthLimits(t *testing.T) {
u := getTestUser()
u.UploadBandwidth = 128
@ -19686,49 +19589,6 @@ func TestWebUserAddMock(t *testing.T) {
assert.Contains(t, rr.Body.String(), "Validation error: could not parse bandwidth limit source")
form.Set("bandwidth_limit_sources1", "127.0.0.1/32")
form.Set("upload_bandwidth_source1", "-1")
form.Set("data_transfer_limit_sources0", "127.0.1.1")
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "could not parse data transfer limit source")
form.Set("data_transfer_limit_sources0", "127.0.1.1/32")
form.Set("upload_data_transfer_source0", "a")
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "invalid upload_data_transfer_source")
form.Set("upload_data_transfer_source0", "0")
form.Set("download_data_transfer_source0", "a")
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "invalid download_data_transfer_source")
form.Set("download_data_transfer_source0", "0")
form.Set("total_data_transfer_source0", "a")
b, contentType, _ = getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
setJWTCookieForReq(req, webToken)
req.Header.Set("Content-Type", contentType)
rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr)
assert.Contains(t, rr.Body.String(), "invalid total_data_transfer_source")
form.Set("total_data_transfer_source0", "0")
form.Set("data_transfer_limit_sources10", "192.168.5.0/24, 10.8.0.0/16")
form.Set("download_data_transfer_source10", "100")
form.Set("upload_data_transfer_source10", "120")
form.Set("data_transfer_limit_sources12", "192.168.3.0/24, 10.8.2.0/24,::1/64")
form.Set("download_data_transfer_source12", "100")
form.Set("upload_data_transfer_source12", "120")
form.Set("total_data_transfer_source12", "200")
// invalid external auth cache size
form.Set("external_auth_cache_time", "a")
b, contentType, _ = getMultipartFormData(form, "", "")
@ -19850,30 +19710,6 @@ func TestWebUserAddMock(t *testing.T) {
}
}
}
if assert.Len(t, newUser.Filters.DataTransferLimits, 3) {
for _, dtLimit := range newUser.Filters.DataTransferLimits {
switch len(dtLimit.Sources) {
case 3:
assert.Equal(t, "192.168.3.0/24", dtLimit.Sources[0])
assert.Equal(t, "10.8.2.0/24", dtLimit.Sources[1])
assert.Equal(t, "::1/64", dtLimit.Sources[2])
assert.Equal(t, int64(0), dtLimit.UploadDataTransfer)
assert.Equal(t, int64(0), dtLimit.DownloadDataTransfer)
assert.Equal(t, int64(200), dtLimit.TotalDataTransfer)
case 2:
assert.Equal(t, "192.168.5.0/24", dtLimit.Sources[0])
assert.Equal(t, "10.8.0.0/16", dtLimit.Sources[1])
assert.Equal(t, int64(120), dtLimit.UploadDataTransfer)
assert.Equal(t, int64(100), dtLimit.DownloadDataTransfer)
assert.Equal(t, int64(0), dtLimit.TotalDataTransfer)
case 1:
assert.Equal(t, "127.0.1.1/32", dtLimit.Sources[0])
assert.Equal(t, int64(0), dtLimit.UploadDataTransfer)
assert.Equal(t, int64(0), dtLimit.DownloadDataTransfer)
assert.Equal(t, int64(0), dtLimit.TotalDataTransfer)
}
}
}
assert.Len(t, newUser.Groups, 3)
assert.Equal(t, sdk.TLSUsernameNone, newUser.Filters.TLSUsername)
req, _ = http.NewRequest(http.MethodDelete, path.Join(userPath, newUser.Username), nil)

View file

@ -3365,6 +3365,92 @@ func TestGetLogEventString(t *testing.T) {
assert.Empty(t, getLogEventString(0))
}
func TestUserQuotaUsage(t *testing.T) {
usage := userQuotaUsage{
QuotaSize: 100,
}
require.True(t, usage.HasQuotaInfo())
require.NotEmpty(t, usage.GetQuotaSize())
providerConf := dataprovider.GetProviderConfig()
quotaTracking := dataprovider.GetQuotaTracking()
providerConf.TrackQuota = 0
err := dataprovider.Close()
assert.NoError(t, err)
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
err = dataprovider.Close()
assert.NoError(t, err)
assert.False(t, usage.HasQuotaInfo())
providerConf.TrackQuota = quotaTracking
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
usage.QuotaSize = 0
assert.False(t, usage.HasQuotaInfo())
assert.Empty(t, usage.GetQuotaSize())
assert.Equal(t, 0, usage.GetQuotaSizePercentage())
assert.False(t, usage.IsQuotaSizeLow())
assert.False(t, usage.IsDiskQuotaLow())
assert.False(t, usage.IsQuotaLow())
usage.UsedQuotaSize = 9
assert.NotEmpty(t, usage.GetQuotaSize())
usage.QuotaSize = 10
assert.True(t, usage.IsQuotaSizeLow())
assert.True(t, usage.IsDiskQuotaLow())
assert.True(t, usage.IsQuotaLow())
usage.DownloadDataTransfer = 1
assert.True(t, usage.HasQuotaInfo())
assert.True(t, usage.HasTranferQuota())
assert.Empty(t, usage.GetQuotaFiles())
assert.Equal(t, 0, usage.GetQuotaFilesPercentage())
usage.QuotaFiles = 1
assert.NotEmpty(t, usage.GetQuotaFiles())
usage.QuotaFiles = 0
usage.UsedQuotaFiles = 9
assert.NotEmpty(t, usage.GetQuotaFiles())
usage.QuotaFiles = 10
usage.DownloadDataTransfer = 0
assert.True(t, usage.IsQuotaFilesLow())
assert.True(t, usage.IsDiskQuotaLow())
assert.False(t, usage.IsTotalTransferQuotaLow())
assert.False(t, usage.IsUploadTransferQuotaLow())
assert.False(t, usage.IsDownloadTransferQuotaLow())
assert.Equal(t, 0, usage.GetTotalTransferQuotaPercentage())
assert.Equal(t, 0, usage.GetUploadTransferQuotaPercentage())
assert.Equal(t, 0, usage.GetDownloadTransferQuotaPercentage())
assert.Empty(t, usage.GetTotalTransferQuota())
assert.Empty(t, usage.GetUploadTransferQuota())
assert.Empty(t, usage.GetDownloadTransferQuota())
usage.TotalDataTransfer = 3
usage.UsedUploadDataTransfer = 1 * 1048576
assert.NotEmpty(t, usage.GetTotalTransferQuota())
usage.TotalDataTransfer = 0
assert.NotEmpty(t, usage.GetTotalTransferQuota())
assert.NotEmpty(t, usage.GetUploadTransferQuota())
usage.UploadDataTransfer = 2
assert.NotEmpty(t, usage.GetUploadTransferQuota())
usage.UsedDownloadDataTransfer = 1 * 1048576
assert.NotEmpty(t, usage.GetDownloadTransferQuota())
usage.DownloadDataTransfer = 2
assert.NotEmpty(t, usage.GetDownloadTransferQuota())
assert.False(t, usage.IsTransferQuotaLow())
usage.UsedDownloadDataTransfer = 8 * 1048576
usage.TotalDataTransfer = 10
assert.True(t, usage.IsTotalTransferQuotaLow())
assert.True(t, usage.IsTransferQuotaLow())
usage.TotalDataTransfer = 0
usage.UploadDataTransfer = 0
usage.DownloadDataTransfer = 0
assert.False(t, usage.IsTransferQuotaLow())
usage.UploadDataTransfer = 10
usage.UsedUploadDataTransfer = 9 * 1048576
assert.True(t, usage.IsUploadTransferQuotaLow())
assert.True(t, usage.IsTransferQuotaLow())
usage.DownloadDataTransfer = 10
usage.UsedDownloadDataTransfer = 9 * 1048576
assert.True(t, usage.IsDownloadTransferQuotaLow())
assert.True(t, usage.IsTransferQuotaLow())
}
func isSharedProviderSupported() bool {
// SQLite shares the implementation with other SQL-based provider but it makes no sense
// to use it outside test cases

View file

@ -1353,50 +1353,6 @@ func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
return permissions
}
func getDataTransferLimitsFromPostFields(r *http.Request) ([]sdk.DataTransferLimit, error) {
var result []sdk.DataTransferLimit
for k := range r.Form {
if strings.HasPrefix(k, "data_transfer_limit_sources") {
sources := getSliceFromDelimitedValues(r.Form.Get(k), ",")
if len(sources) > 0 {
dtLimit := sdk.DataTransferLimit{
Sources: sources,
}
idx := strings.TrimPrefix(k, "data_transfer_limit_sources")
ul := r.Form.Get(fmt.Sprintf("upload_data_transfer_source%v", idx))
dl := r.Form.Get(fmt.Sprintf("download_data_transfer_source%v", idx))
total := r.Form.Get(fmt.Sprintf("total_data_transfer_source%v", idx))
if ul != "" {
dataUL, err := strconv.ParseInt(ul, 10, 64)
if err != nil {
return result, fmt.Errorf("invalid upload_data_transfer_source%v %q: %w", idx, ul, err)
}
dtLimit.UploadDataTransfer = dataUL
}
if dl != "" {
dataDL, err := strconv.ParseInt(dl, 10, 64)
if err != nil {
return result, fmt.Errorf("invalid download_data_transfer_source%v %q: %w", idx, dl, err)
}
dtLimit.DownloadDataTransfer = dataDL
}
if total != "" {
dataTotal, err := strconv.ParseInt(total, 10, 64)
if err != nil {
return result, fmt.Errorf("invalid total_data_transfer_source%v %q: %w", idx, total, err)
}
dtLimit.TotalDataTransfer = dataTotal
}
result = append(result, dtLimit)
}
}
}
return result, nil
}
func getBandwidthLimitsFromPostFields(r *http.Request) ([]sdk.BandwidthLimit, error) {
var result []sdk.BandwidthLimit
@ -1534,10 +1490,6 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
if err != nil {
return filters, err
}
dtLimits, err := getDataTransferLimitsFromPostFields(r)
if err != nil {
return filters, err
}
maxFileSize, err := util.ParseBytes(r.Form.Get("max_upload_file_size"))
if err != nil {
return filters, fmt.Errorf("invalid max upload file size: %w", err)
@ -1558,7 +1510,6 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
filters.FTPSecurity = 1
}
filters.BandwidthLimits = bwLimits
filters.DataTransferLimits = dtLimits
filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
filters.DeniedLoginMethods = r.Form["denied_login_methods"]
@ -1614,7 +1565,7 @@ func getS3Config(r *http.Request) (vfs.S3FsConfig, error) {
config.Endpoint = strings.TrimSpace(r.Form.Get("s3_endpoint"))
config.StorageClass = strings.TrimSpace(r.Form.Get("s3_storage_class"))
config.ACL = strings.TrimSpace(r.Form.Get("s3_acl"))
config.KeyPrefix = strings.TrimSpace(r.Form.Get("s3_key_prefix"))
config.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(r.Form.Get("s3_key_prefix"), "/"))
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64)
if err != nil {
return config, fmt.Errorf("invalid s3 upload part size: %w", err)
@ -1650,7 +1601,7 @@ func getGCSConfig(r *http.Request) (vfs.GCSFsConfig, error) {
config.Bucket = strings.TrimSpace(r.Form.Get("gcs_bucket"))
config.StorageClass = strings.TrimSpace(r.Form.Get("gcs_storage_class"))
config.ACL = strings.TrimSpace(r.Form.Get("gcs_acl"))
config.KeyPrefix = strings.TrimSpace(r.Form.Get("gcs_key_prefix"))
config.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(r.Form.Get("gcs_key_prefix"), "/"))
uploadPartSize, err := strconv.ParseInt(r.Form.Get("gcs_upload_part_size"), 10, 64)
if err == nil {
config.UploadPartSize = uploadPartSize
@ -1732,7 +1683,7 @@ func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
config.AccountKey = getSecretFromFormField(r, "az_account_key")
config.SASURL = getSecretFromFormField(r, "az_sas_url")
config.Endpoint = strings.TrimSpace(r.Form.Get("az_endpoint"))
config.KeyPrefix = strings.TrimSpace(r.Form.Get("az_key_prefix"))
config.KeyPrefix = strings.TrimSpace(strings.TrimPrefix(r.Form.Get("az_key_prefix"), "/"))
config.AccessTier = strings.TrimSpace(r.Form.Get("az_access_tier"))
config.UseEmulator = r.Form.Get("az_use_emulator") != ""
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("az_upload_part_size"), 10, 64)

View file

@ -21,6 +21,7 @@ import (
"fmt"
"html/template"
"io"
"math"
"net/http"
"net/url"
"os"
@ -155,6 +156,7 @@ type filesPage struct {
Error string
Paths []dirMapping
HasIntegrations bool
QuotaUsage *userQuotaUsage
}
type shareLoginPage struct {
@ -229,6 +231,185 @@ type clientSharePage struct {
IsAdd bool
}
type userQuotaUsage struct {
QuotaSize int64
QuotaFiles int
UsedQuotaSize int64
UsedQuotaFiles int
UploadDataTransfer int64
DownloadDataTransfer int64
TotalDataTransfer int64
UsedUploadDataTransfer int64
UsedDownloadDataTransfer int64
}
func (u *userQuotaUsage) HasQuotaInfo() bool {
if dataprovider.GetQuotaTracking() == 0 {
return false
}
if u.HasDiskQuota() {
return true
}
return u.HasTranferQuota()
}
func (u *userQuotaUsage) HasDiskQuota() bool {
if u.QuotaSize > 0 || u.UsedQuotaSize > 0 {
return true
}
return u.QuotaFiles > 0 || u.UsedQuotaFiles > 0
}
func (u *userQuotaUsage) HasTranferQuota() bool {
if u.TotalDataTransfer > 0 || u.UploadDataTransfer > 0 || u.DownloadDataTransfer > 0 {
return true
}
return u.UsedDownloadDataTransfer > 0 || u.UsedUploadDataTransfer > 0
}
func (u *userQuotaUsage) GetQuotaSize() string {
if u.QuotaSize > 0 {
return fmt.Sprintf("%s/%s", util.ByteCountIEC(u.UsedQuotaSize), util.ByteCountIEC(u.QuotaSize))
}
if u.UsedQuotaSize > 0 {
return util.ByteCountIEC(u.UsedQuotaSize)
}
return ""
}
func (u *userQuotaUsage) GetQuotaFiles() string {
if u.QuotaFiles > 0 {
return fmt.Sprintf("%d/%d", u.UsedQuotaFiles, u.QuotaFiles)
}
if u.UsedQuotaFiles > 0 {
return strconv.FormatInt(int64(u.UsedQuotaFiles), 10)
}
return ""
}
func (u *userQuotaUsage) GetQuotaSizePercentage() int {
if u.QuotaSize > 0 {
return int(math.Round(100 * float64(u.UsedQuotaSize) / float64(u.QuotaSize)))
}
return 0
}
func (u *userQuotaUsage) GetQuotaFilesPercentage() int {
if u.QuotaFiles > 0 {
return int(math.Round(100 * float64(u.UsedQuotaFiles) / float64(u.QuotaFiles)))
}
return 0
}
func (u *userQuotaUsage) IsQuotaSizeLow() bool {
return u.GetQuotaSizePercentage() > 85
}
func (u *userQuotaUsage) IsQuotaFilesLow() bool {
return u.GetQuotaFilesPercentage() > 85
}
func (u *userQuotaUsage) IsDiskQuotaLow() bool {
return u.IsQuotaSizeLow() || u.IsQuotaFilesLow()
}
func (u *userQuotaUsage) GetTotalTransferQuota() string {
total := u.UsedUploadDataTransfer + u.UsedDownloadDataTransfer
if u.TotalDataTransfer > 0 {
return fmt.Sprintf("%s/%s", util.ByteCountIEC(total), util.ByteCountIEC(u.TotalDataTransfer*1048576))
}
if total > 0 {
return util.ByteCountIEC(total)
}
return ""
}
func (u *userQuotaUsage) GetUploadTransferQuota() string {
if u.UploadDataTransfer > 0 {
return fmt.Sprintf("%s/%s", util.ByteCountIEC(u.UsedUploadDataTransfer),
util.ByteCountIEC(u.UploadDataTransfer*1048576))
}
if u.UsedUploadDataTransfer > 0 {
return util.ByteCountIEC(u.UsedUploadDataTransfer)
}
return ""
}
func (u *userQuotaUsage) GetDownloadTransferQuota() string {
if u.DownloadDataTransfer > 0 {
return fmt.Sprintf("%s/%s", util.ByteCountIEC(u.UsedDownloadDataTransfer),
util.ByteCountIEC(u.DownloadDataTransfer*1048576))
}
if u.UsedDownloadDataTransfer > 0 {
return util.ByteCountIEC(u.UsedDownloadDataTransfer)
}
return ""
}
func (u *userQuotaUsage) GetTotalTransferQuotaPercentage() int {
if u.TotalDataTransfer > 0 {
return int(math.Round(100 * float64(u.UsedDownloadDataTransfer+u.UsedUploadDataTransfer) / float64(u.TotalDataTransfer*1048576)))
}
return 0
}
func (u *userQuotaUsage) GetUploadTransferQuotaPercentage() int {
if u.UploadDataTransfer > 0 {
return int(math.Round(100 * float64(u.UsedUploadDataTransfer) / float64(u.UploadDataTransfer*1048576)))
}
return 0
}
func (u *userQuotaUsage) GetDownloadTransferQuotaPercentage() int {
if u.DownloadDataTransfer > 0 {
return int(math.Round(100 * float64(u.UsedDownloadDataTransfer) / float64(u.DownloadDataTransfer*1048576)))
}
return 0
}
func (u *userQuotaUsage) IsTotalTransferQuotaLow() bool {
if u.TotalDataTransfer > 0 {
return u.GetTotalTransferQuotaPercentage() > 85
}
return false
}
func (u *userQuotaUsage) IsUploadTransferQuotaLow() bool {
if u.UploadDataTransfer > 0 {
return u.GetUploadTransferQuotaPercentage() > 85
}
return false
}
func (u *userQuotaUsage) IsDownloadTransferQuotaLow() bool {
if u.DownloadDataTransfer > 0 {
return u.GetDownloadTransferQuotaPercentage() > 85
}
return false
}
func (u *userQuotaUsage) IsTransferQuotaLow() bool {
return u.IsTotalTransferQuotaLow() || u.IsUploadTransferQuotaLow() || u.IsDownloadTransferQuotaLow()
}
func (u *userQuotaUsage) IsQuotaLow() bool {
return u.IsDiskQuotaLow() || u.IsTransferQuotaLow()
}
func newUserQuotaUsage(u *dataprovider.User) *userQuotaUsage {
return &userQuotaUsage{
QuotaSize: u.QuotaSize,
QuotaFiles: u.QuotaFiles,
UsedQuotaSize: u.UsedQuotaSize,
UsedQuotaFiles: u.UsedQuotaFiles,
TotalDataTransfer: u.TotalDataTransfer,
UploadDataTransfer: u.UploadDataTransfer,
DownloadDataTransfer: u.DownloadDataTransfer,
UsedUploadDataTransfer: u.UsedUploadDataTransfer,
UsedDownloadDataTransfer: u.UsedDownloadDataTransfer,
}
}
func getFileObjectURL(baseDir, name, baseWebPath string) string {
return fmt.Sprintf("%v?path=%v&_=%v", baseWebPath, url.QueryEscape(path.Join(baseDir, name)), time.Now().UTC().Unix())
}
@ -595,7 +776,7 @@ func (s *httpdServer) renderUploadToSharePage(w http.ResponseWriter, r *http.Req
renderClientTemplate(w, templateUploadToShare, data)
}
func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user dataprovider.User,
func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user *dataprovider.User,
hasIntegrations bool,
) {
data := filesPage{
@ -615,6 +796,7 @@ func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, di
CanShare: user.CanManageShares(),
HasIntegrations: hasIntegrations,
Paths: getDirMapping(dirName, webClientFilesPath),
QuotaUsage: newUserQuotaUsage(user),
}
renderClientTemplate(w, templateClientFiles, data)
}
@ -964,11 +1146,11 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
}
if err != nil {
s.renderFilesPage(w, r, path.Dir(name), fmt.Sprintf("unable to stat file %q: %v", name, err),
user, len(s.binding.WebClientIntegrations) > 0)
&user, len(s.binding.WebClientIntegrations) > 0)
return
}
if info.IsDir() {
s.renderFilesPage(w, r, name, "", user, len(s.binding.WebClientIntegrations) > 0)
s.renderFilesPage(w, r, name, "", &user, len(s.binding.WebClientIntegrations) > 0)
return
}
if status, err := downloadFile(w, r, connection, name, info, false, nil); err != nil && status != 0 {
@ -977,7 +1159,7 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
s.renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "")
return
}
s.renderFilesPage(w, r, path.Dir(name), err.Error(), user, len(s.binding.WebClientIntegrations) > 0)
s.renderFilesPage(w, r, path.Dir(name), err.Error(), &user, len(s.binding.WebClientIntegrations) > 0)
}
}
}

View file

@ -2487,9 +2487,6 @@ func compareUserFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters
if err := compareUserBandwidthLimitFilters(expected, actual); err != nil {
return err
}
if err := compareUserDataTransferLimitFilters(expected, actual); err != nil {
return err
}
return compareUserFilePatternsFilters(expected, actual)
}
@ -2505,30 +2502,6 @@ func checkFilterMatch(expected []string, actual []string) bool {
return true
}
func compareUserDataTransferLimitFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
if len(expected.DataTransferLimits) != len(actual.DataTransferLimits) {
return errors.New("data transfer limits filters mismatch")
}
for idx, l := range expected.DataTransferLimits {
if actual.DataTransferLimits[idx].UploadDataTransfer != l.UploadDataTransfer {
return errors.New("data transfer limit upload_data_transfer mismatch")
}
if actual.DataTransferLimits[idx].DownloadDataTransfer != l.DownloadDataTransfer {
return errors.New("data transfer limit download_data_transfer mismatch")
}
if actual.DataTransferLimits[idx].TotalDataTransfer != l.TotalDataTransfer {
return errors.New("data transfer limit total_data_transfer mismatch")
}
for _, source := range actual.DataTransferLimits[idx].Sources {
if !util.Contains(l.Sources, source) {
return errors.New("data transfer limit source mismatch")
}
}
}
return nil
}
func compareUserBandwidthLimitFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFilters) error {
if len(expected.BandwidthLimits) != len(actual.BandwidthLimits) {
return errors.New("bandwidth limits filters mismatch")

View file

@ -17,7 +17,7 @@ package version
import "strings"
const version = "2.5.1-dev"
const version = "2.5.4"
var (
commit = ""

View file

@ -33,7 +33,7 @@ paths:
200:
description: successful operation
content:
application/json; charset=utf-8:
application/json:
schema:
$ref: '#/components/schemas/FileInfo'
401:
@ -350,7 +350,7 @@ paths:
200:
description: successful operation
content:
application/json; charset=utf-8:
application/json:
schema:
type: array
items:
@ -384,7 +384,7 @@ paths:
200:
description: successful operation
content:
application/json; charset=utf-8:
application/json:
schema:
type: object
properties:
@ -424,7 +424,7 @@ paths:
200:
description: successful operation
content:
application/json; charset=utf-8:
application/json:
schema:
type: object
properties:
@ -459,7 +459,7 @@ paths:
200:
description: successful operation
content:
application/json; charset=utf-8:
application/json:
schema:
$ref: '#/components/schemas/StatVFS'
401:
@ -479,61 +479,61 @@ components:
OKResponse:
description: successful operation
content:
application/json; charset=utf-8:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
BadRequest:
description: Bad Request
content:
application/json; charset=utf-8:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
Unauthorized:
description: Unauthorized
content:
application/json; charset=utf-8:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
Forbidden:
description: Forbidden
content:
application/json; charset=utf-8:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
NotFound:
description: Not Found
content:
application/json; charset=utf-8:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
NotImplemented:
description: Not Implemented
content:
application/json; charset=utf-8:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
Conflict:
description: Conflict
content:
application/json; charset=utf-8:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
RequestEntityTooLarge:
description: Request Entity Too Large, max allowed size exceeded
content:
application/json; charset=utf-8:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
InternalServerError:
description: Internal Server Error
content:
application/json; charset=utf-8:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
DefaultResponse:
description: Unexpected Error
content:
application/json; charset=utf-8:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
schemas:

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
#!/bin/bash
NFPM_VERSION=2.30.1
NFPM_VERSION=2.32.0
NFPM_ARCH=${NFPM_ARCH:-amd64}
if [ -z ${SFTPGO_VERSION} ]
then

View file

@ -459,7 +459,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify(data),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('#spinnerModal').modal('hide');
@ -526,7 +526,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify(data),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('#spinnerModal').modal('hide');

View file

@ -216,7 +216,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<input type="text" class="form-control" id="idS3KeyPrefix" name="s3_key_prefix" placeholder=""
value="{{.S3Config.KeyPrefix}}" aria-describedby="S3KeyPrefixHelpBlock">
<small id="S3KeyPrefixHelpBlock" class="form-text text-muted">
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/".
Similar to a chroot for local filesystem. Example: "somedir/subdir/".
</small>
</div>
</div>
@ -298,7 +298,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<input type="text" class="form-control" id="idGCSKeyPrefix" name="gcs_key_prefix" placeholder=""
value="{{.GCSConfig.KeyPrefix}}" aria-describedby="GCSKeyPrefixHelpBlock">
<small id="GCSKeyPrefixHelpBlock" class="form-text text-muted">
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/".
Similar to a chroot for local filesystem. Example: "somedir/subdir/".
</small>
</div>
</div>
@ -420,7 +420,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<input type="text" class="form-control" id="idAzKeyPrefix" name="az_key_prefix" placeholder=""
value="{{.AzBlobConfig.KeyPrefix}}" aria-describedby="AzKeyPrefixHelpBlock">
<small id="AzKeyPrefixHelpBlock" class="form-text text-muted">
Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/".
Similar to a chroot for local filesystem. Example: "somedir/subdir/".
</small>
</div>
</div>

View file

@ -222,7 +222,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
<div class="card-body">
<h6 class="card-title mb-4">Comma separated denied or allowed files/directories, based on shell patterns.</h6>
<p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories</p>
<p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories. Setting a denied pattern as "*" and allowed pattern/s for the same directory you can create denied except rules, but note that if you allow a directory, everything in it will be allowed unless more specific patterns/permissions are defined.</p>
<div class="form-group row">
<div class="col-md-12 form_field_patterns_outer">
{{range $idx, $pattern := .Group.UserSettings.Filters.GetFlatFilePatterns -}}
@ -554,105 +554,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
</div>
<div class="card bg-light mb-3">
<div class="card-header">
<b>Per-source data transfer limits</b>
</div>
<div class="card-body">
<div class="form-group row">
<div class="col-md-12 form_field_dtlimits_outer">
{{range $idx, $dtLimit := .Group.UserSettings.Filters.DataTransferLimits -}}
<div class="row form_field_dtlimits_outer_row">
<div class="form-group col-md-5">
<textarea class="form-control" id="idDataTransferLimitSources{{$idx}}" name="data_transfer_limit_sources{{$idx}}" rows="4" placeholder=""
aria-describedby="dtLimitSourcesHelpBlock{{$idx}}">{{$dtLimit.GetSourcesAsString}}</textarea>
<small id="dtLimitSourcesHelpBlock{{$idx}}" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idUploadTransferSource{{$idx}}" name="upload_data_transfer_source{{$idx}}"
placeholder="" value="{{$dtLimit.UploadDataTransfer}}" min="0" aria-describedby="ulDtHelpBlock{{$idx}}">
<small id="ulDtHelpBlock{{$idx}}" class="form-text text-muted">
UL (MB). 0 means no limit
</small>
</div>
<div class="form-group">
<input type="number" class="form-control" id="idDownloadTransferSource{{$idx}}" name="download_data_transfer_source{{$idx}}"
placeholder="" value="{{$dtLimit.DownloadDataTransfer}}" min="0" aria-describedby="dlDtHelpBlock{{$idx}}">
<small id="dlDtHelpBlock{{$idx}}" class="form-text text-muted">
DL (MB). 0 means no limit
</small>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idTotalTransferSource{{$idx}}" name="total_data_transfer_source{{$idx}}"
placeholder="" value="{{$dtLimit.TotalDataTransfer}}" min="0" aria-describedby="totalDtHelpBlock{{$idx}}">
<small id="totalDtHelpBlock{{$idx}}" class="form-text text-muted">
Total (MB). 0 means no limit
</small>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_dtlimits_outer_row">
<div class="form-group col-md-5">
<textarea class="form-control" id="idDataTransferLimitSources0" name="data_transfer_limit_sources0" rows="4" placeholder=""
aria-describedby="dtLimitSourcesHelpBlock0"></textarea>
<small id="dtLimitSourcesHelpBlock0" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idUploadTransferSource0" name="upload_data_transfer_source0"
placeholder="" value="" min="0" aria-describedby="ulDtHelpBlock0">
<small id="ulDtHelpBlock0" class="form-text text-muted">
UL (MB). 0 means no limit
</small>
</div>
<div class="form-group">
<input type="number" class="form-control" id="idDownloadTransferSource0" name="download_data_transfer_source0"
placeholder="" value="" min="0" aria-describedby="dlDtHelpBlock0">
<small id="dlDtHelpBlock0" class="form-text text-muted">
DL (MB). 0 means no limit
</small>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idTotalTransferSource0" name="total_data_transfer_source0"
placeholder="" value="" min="0" aria-describedby="totalDtHelpBlock0">
<small id="totalDtHelpBlock0" class="form-text text-muted">
Total (MB). 0 means no limit
</small>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_dtlimit_field_btn">
<i class="fas fa-plus"></i> Add new data transfer limit
</button>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -166,7 +166,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"config_name": $('#idConfig option:selected').val()}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('.totpDisable').hide();
@ -209,7 +209,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"passcode": passcode, "config_name": $('#idConfig option:selected').val(), "secret": $('#idSecret').text()}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
totpSave();
@ -242,7 +242,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"enabled": true, "config_name": $('#idConfig option:selected').val(), "secret": {"status": "Plain", "payload": $('#idSecret').text()}}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('#successTOTPTxt').text("Configuration saved");
@ -284,7 +284,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"enabled": false}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
location.reload();
@ -357,7 +357,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
type: 'POST',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('.viewRecoveryCodes').hide();

View file

@ -137,58 +137,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
$(this).closest(".form_field_bwlimits_outer_row").remove();
});
$("body").on("click", ".add_new_dtlimit_field_btn", function () {
let index = $(".form_field_dtlimits_outer").find(".form_field_dtlimits_outer_row").length;
while (document.getElementById("idDataTransferLimitSources"+index) != null){
index++;
}
$(".form_field_dtlimits_outer").append(`
<div class="row form_field_dtlimits_outer_row">
<div class="form-group col-md-5">
<textarea class="form-control" id="idDataTransferLimitSources${index}" name="data_transfer_limit_sources${index}" rows="4" placeholder=""
aria-describedby="dtLimitSourcesHelpBlock${index}"></textarea>
<small id="dtLimitSourcesHelpBlock${index}" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idUploadTransferSource${index}" name="upload_data_transfer_source${index}"
placeholder="" value="" min="0" aria-describedby="ulDtHelpBlock${index}">
<small id="ulDtHelpBlock${index}" class="form-text text-muted">
UL (MB). 0 means no limit
</small>
</div>
<div class="form-group">
<input type="number" class="form-control" id="idDownloadTransferSource${index}" name="download_data_transfer_source${index}"
placeholder="" value="" min="0" aria-describedby="dlDtHelpBlock${index}">
<small id="dlDtHelpBlock${index}" class="form-text text-muted">
DL (MB). 0 means no limit
</small>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idTotalTransferSource${index}" name="total_data_transfer_source${index}"
placeholder="" value="" min="0" aria-describedby="totalDtHelpBlock${index}">
<small id="totalDtHelpBlock${index}" class="form-text text-muted">
Total (MB). 0 means no limit
</small>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`);
});
$("body").on("click", ".remove_dtlimit_btn_frm_field", function () {
$(this).closest(".form_field_dtlimits_outer_row").remove();
});
$("body").on("click", ".add_new_pattern_field_btn", function () {
let index = $(".form_field_patterns_outer").find(".form_field_patterns_outer_row").length;
while (document.getElementById("idPatternPath"+index) != null){

View file

@ -502,7 +502,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
<div class="card-body">
<h6 class="card-title mb-4">Comma separated denied or allowed files/directories, based on shell patterns.</h6>
<p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories</p>
<p class="card-text">Match is case insensitive, set you patterns as lowercase. Denied entries are visible in directory listing by default, you can hide them by setting the "Hidden" policy, but please be aware that this may cause performance issues for large directories. Setting a denied pattern as "*" and allowed pattern/s for the same directory you can create denied except rules, but note that if you allow a directory, everything in it will be allowed unless more specific patterns/permissions are defined.</p>
<div class="form-group row">
<div class="col-md-12 form_field_patterns_outer">
{{range $idx, $pattern := .User.Filters.GetFlatFilePatterns -}}
@ -834,105 +834,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
</div>
<div class="card bg-light mb-3">
<div class="card-header">
<b>Per-source data transfer limits</b>
</div>
<div class="card-body">
<div class="form-group row">
<div class="col-md-12 form_field_dtlimits_outer">
{{range $idx, $dtLimit := .User.Filters.DataTransferLimits -}}
<div class="row form_field_dtlimits_outer_row">
<div class="form-group col-md-5">
<textarea class="form-control" id="idDataTransferLimitSources{{$idx}}" name="data_transfer_limit_sources{{$idx}}" rows="4" placeholder=""
aria-describedby="dtLimitSourcesHelpBlock{{$idx}}">{{$dtLimit.GetSourcesAsString}}</textarea>
<small id="dtLimitSourcesHelpBlock{{$idx}}" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idUploadTransferSource{{$idx}}" name="upload_data_transfer_source{{$idx}}"
placeholder="" value="{{$dtLimit.UploadDataTransfer}}" min="0" aria-describedby="ulDtHelpBlock{{$idx}}">
<small id="ulDtHelpBlock{{$idx}}" class="form-text text-muted">
UL (MB). 0 means no limit
</small>
</div>
<div class="form-group">
<input type="number" class="form-control" id="idDownloadTransferSource{{$idx}}" name="download_data_transfer_source{{$idx}}"
placeholder="" value="{{$dtLimit.DownloadDataTransfer}}" min="0" aria-describedby="dlDtHelpBlock{{$idx}}">
<small id="dlDtHelpBlock{{$idx}}" class="form-text text-muted">
DL (MB). 0 means no limit
</small>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idTotalTransferSource{{$idx}}" name="total_data_transfer_source{{$idx}}"
placeholder="" value="{{$dtLimit.TotalDataTransfer}}" min="0" aria-describedby="totalDtHelpBlock{{$idx}}">
<small id="totalDtHelpBlock{{$idx}}" class="form-text text-muted">
Total (MB). 0 means no limit
</small>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{else}}
<div class="row form_field_dtlimits_outer_row">
<div class="form-group col-md-5">
<textarea class="form-control" id="idDataTransferLimitSources0" name="data_transfer_limit_sources0" rows="4" placeholder=""
aria-describedby="dtLimitSourcesHelpBlock0"></textarea>
<small id="dtLimitSourcesHelpBlock0" class="form-text text-muted">
Comma separated IP/Mask in CIDR format, example: "192.168.1.0/24,10.8.0.100/32"
</small>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idUploadTransferSource0" name="upload_data_transfer_source0"
placeholder="" value="" min="0" aria-describedby="ulDtHelpBlock0">
<small id="ulDtHelpBlock0" class="form-text text-muted">
UL (MB). 0 means no limit
</small>
</div>
<div class="form-group">
<input type="number" class="form-control" id="idDownloadTransferSource0" name="download_data_transfer_source0"
placeholder="" value="" min="0" aria-describedby="dlDtHelpBlock0">
<small id="dlDtHelpBlock0" class="form-text text-muted">
DL (MB). 0 means no limit
</small>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<input type="number" class="form-control" id="idTotalTransferSource0" name="total_data_transfer_source0"
placeholder="" value="" min="0" aria-describedby="totalDtHelpBlock0">
<small id="totalDtHelpBlock0" class="form-text text-muted">
Total (MB). 0 means no limit
</small>
</div>
</div>
<div class="form-group col-md-1">
<button class="btn btn-circle btn-danger remove_dtlimit_btn_frm_field">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="row mx-1">
<button type="button" class="btn btn-secondary add_new_dtlimit_field_btn">
<i class="fas fa-plus"></i> Add new data transfer limit
</button>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -34,6 +34,82 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</style>
{{end}}
{{define "additionalnavitems"}}
{{if .QuotaUsage.HasQuotaInfo}}
<li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="quotaDropdown" role="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mr-2 d-none d-lg-inline text-gray-600 small">Quota</span>
{{if .QuotaUsage.IsQuotaLow}}
<i class="fas fa-exclamation-triangle fa-fw"></i>
{{else}}
<i class="fas fa-info fa-fw"></i>
{{end}}
</a>
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
ria-labelledby="alertsDropdown">
<h6 class="dropdown-header">
Quota usage
</h6>
{{ if .QuotaUsage.HasDiskQuota}}
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle {{if .QuotaUsage.IsDiskQuotaLow}}bg-warning{{else}}bg-success{{end}}">
<i class="fas fa-hdd text-white"></i>
</div>
</div>
<div>
<div class="small text-gray-500">Disk quota</div>
{{$size := .QuotaUsage.GetQuotaSize}}
{{$files := .QuotaUsage.GetQuotaFiles}}
{{if $size}}
{{$percentage := .QuotaUsage.GetQuotaSizePercentage}}
<span class="font-weight-bold {{if .QuotaUsage.IsQuotaSizeLow}}text-warning{{end}}">Size: {{$size}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
{{if $files}}<br>{{end}}
{{end}}
{{if $files}}
{{$percentage := .QuotaUsage.GetQuotaFilesPercentage}}
<span class="font-weight-bold {{if .QuotaUsage.IsQuotaFilesLow}}text-warning{{end}}">Files: {{$files}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
{{end}}
</div>
</a>
{{end}}
{{ if .QuotaUsage.HasTranferQuota}}
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle {{if .QuotaUsage.IsTransferQuotaLow}}bg-warning{{else}}bg-success{{end}}">
<i class="fas fa-exchange-alt text-white"></i>
</div>
</div>
<div>
<div class="small text-gray-500">Transfer quota</div>
{{$total := .QuotaUsage.GetTotalTransferQuota}}
{{$upload := .QuotaUsage.GetUploadTransferQuota}}
{{$download := .QuotaUsage.GetDownloadTransferQuota}}
{{if $total}}
{{$percentage := .QuotaUsage.GetTotalTransferQuotaPercentage}}
<span class="font-weight-bold {{if .QuotaUsage.IsTotalTransferQuotaLow}}text-warning{{end}}">Total: {{$total}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
{{if or $upload $download}}<br>{{end}}
{{end}}
{{if $download}}
{{$percentage := .QuotaUsage.GetDownloadTransferQuotaPercentage}}
<span class="font-weight-bold {{if .QuotaUsage.IsDownloadTransferQuotaLow}}text-warning{{end}}">Download: {{$download}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
{{if $upload}}<br>{{end}}
{{end}}
{{if $upload}}
{{$percentage := .QuotaUsage.GetUploadTransferQuotaPercentage}}
<span class="font-weight-bold {{if .QuotaUsage.IsUploadTransferQuotaLow}}text-warning{{end}}">Upload: {{$upload}}{{if gt $percentage 0}} ({{$percentage}}%){{end}}</span>
{{end}}
</div>
</a>
{{end}}
</div>
</li>
<div class="topbar-divider d-none d-sm-block"></div>
{{end}}
{{end}}
{{define "page_body"}}
<div id="errorMsg" class="alert alert-warning alert-dismissible fade show" style="display: none;" role="alert">
<span id="errorTxt"></span>

View file

@ -192,7 +192,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"config_name": $('#idConfig option:selected').val()}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('.totpDisable').hide();
@ -236,7 +236,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"passcode": passcode, "config_name": $('#idConfig option:selected').val(), "secret": $('#idSecret').text()}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
totpSave();
@ -273,7 +273,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"enabled": true, "config_name": $('#idConfig option:selected').val(), "protocols": protocolsArray, "secret": {"status": "Plain", "payload": $('#idSecret').text()}}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('#successTOTPTxt').text("Configuration saved");
@ -318,7 +318,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"protocols": protocolsArray}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('#successTOTPTxt').text("Protocols updated");
@ -356,7 +356,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
data: JSON.stringify({"enabled": false}),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
location.reload();
@ -429,7 +429,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
type: 'POST',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
dataType: 'json',
contentType: 'application/json; charset=utf-8',
contentType: 'application/json',
timeout: 15000,
success: function (result) {
$('.viewRecoveryCodes').hide();

View file

@ -98,4 +98,16 @@ Filename: "{app}\{#MyAppExeName}"; Parameters: "service uninstall"; Flags: runhi
Filename: "netsh"; Parameters: "advfirewall firewall delete rule name=""SFTPGo Service"""; Flags: runhidden; RunOnceId: "Remove SFTPGo firewall rule"
[Messages]
FinishedLabel=Setup has finished installing SFTPGo on your computer. SFTPGo should already be running as a Windows service, it uses TCP port 8080 for HTTP service and TCP port 2022 for SFTP service by default, make sure the configured ports are not used by other services or edit the configuration according to your needs.
FinishedLabel=Setup has finished installing SFTPGo on your computer. SFTPGo should already be running as a Windows service, it uses TCP port 8080 for HTTP service and TCP port 2022 for SFTP service by default, make sure the configured ports are not used by other services or edit the configuration according to your needs.
[Code]
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
Code: Integer;
begin
if (FileExists(ExpandConstant('{app}\{#MyAppExeName}'))) then
begin
Exec(ExpandConstant('{app}\{#MyAppExeName}'), 'service stop', '', SW_HIDE, ewWaitUntilTerminated, Code);
end
end;