mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 00:50:31 +00:00
web admin: add backup/restore
This commit is contained in:
parent
820169c5c6
commit
80f5ccd357
17 changed files with 1231 additions and 780 deletions
|
@ -1,6 +1,6 @@
|
|||
# Defender
|
||||
|
||||
The experimental built-in `defender` allows you to configure an auto-blocking policy for SFTPGo and thus helps to prevent DoS (Denial of Service) and brute force password guessing.
|
||||
The built-in `defender` allows you to configure an auto-blocking policy for SFTPGo and thus helps to prevent DoS (Denial of Service) and brute force password guessing.
|
||||
|
||||
If enabled it will protect SFTP, FTP and WebDAV services and it will automatically block hosts (IP addresses) that continually fail to log in or attempt to connect.
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ We use [Go CDK](https://gocloud.dev/howto/secrets/) to access several key manage
|
|||
|
||||
### Local provider
|
||||
|
||||
If the `url` is empty SFTPGo uses local encryption for keeping secrets. Internally, it uses the [NaCl secret box](https://godoc.org/golang.org/x/crypto/nacl/secretbox) algorithm to perform encryption and authentication.
|
||||
If the `url` is empty SFTPGo uses local encryption for keeping secrets. Internally, it uses the [NaCl secret box](https://pkg.go.dev/golang.org/x/crypto/nacl/secretbox) algorithm to perform encryption and authentication.
|
||||
|
||||
We first generate a random key, then the per-object encryption key is derived from this random key in the following way:
|
||||
|
||||
|
|
42
go.sum
42
go.sum
|
@ -57,32 +57,25 @@ github.com/Azure/azure-storage-blob-go v0.12.0 h1:7bFXA1QB+lOK2/ASWHhp6/vnxjaeeZ
|
|||
github.com/Azure/azure-storage-blob-go v0.12.0/go.mod h1:A0u4VjtpgZJ7Y7um/+ix2DHBuEKFC6sEIlj0xc13a4Q=
|
||||
github.com/Azure/go-amqp v0.13.0/go.mod h1:qj+o8xPCz9tMSbQ83Vp8boHahuRDl5mkNHyt1xlxUTs=
|
||||
github.com/Azure/go-amqp v0.13.1/go.mod h1:qj+o8xPCz9tMSbQ83Vp8boHahuRDl5mkNHyt1xlxUTs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest/autorest v0.11.3/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
|
||||
github.com/Azure/go-autorest/autorest v0.11.7/go.mod h1:V6p3pKZx1KKkJubbxnDWrzNhEIfOy/pTGasLqzHIPHs=
|
||||
github.com/Azure/go-autorest/autorest v0.11.9/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw=
|
||||
github.com/Azure/go-autorest/autorest v0.11.12 h1:gI8ytXbxMfI+IVbI9mP2JGCTXIuhHLgRlvQ9X4PsnHE=
|
||||
github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.2/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.4/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.6 h1:d3pSDwvBWBLqdA91u+keH1zs1cCEzrQdHKY6iqbQNkE=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.6/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.3/go.mod h1:4bJZhUhcq8LB20TruwHbAQsmUs2Xh+QR7utuJpLXX3A=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=
|
||||
github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
|
||||
|
@ -92,7 +85,6 @@ github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go
|
|||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
|
@ -191,13 +183,11 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
|
|||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fclairamb/ftpserverlib v0.12.0 h1:vud3Q4v/rLZU5CfIDFaXq7ST2+V9BF5cKjzNWPN18c4=
|
||||
github.com/fclairamb/ftpserverlib v0.12.0/go.mod h1:X6sAMSYtN0YDPu+nHfyE9dsKPUOrEZ8O5EMgt1xvPwk=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
|
||||
github.com/frankban/quicktest v1.11.2 h1:mjwHjStlXWibxOohM7HYieIViKyh56mmt3+6viyhDDI=
|
||||
github.com/frankban/quicktest v1.11.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
|
@ -218,15 +208,12 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
|
|||
github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo=
|
||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
||||
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
|
||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
|
@ -237,7 +224,6 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
|
|||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
|
@ -293,16 +279,12 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-replayers/grpcreplay v1.0.0 h1:B5kVOzJ1hBgnevTgIWhSTatQ3608yu/2NnU0Ta1d0kY=
|
||||
github.com/google/go-replayers/grpcreplay v1.0.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE=
|
||||
github.com/google/go-replayers/httpreplay v0.1.2 h1:HCfx+dQzwN9XbGTHF8qJ+67WN8glL9FTWV5rraCJ/jU=
|
||||
github.com/google/go-replayers/httpreplay v0.1.2/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible h1:xmapqc1AyLoB+ddYT6r04bD9lIjlOqGaREovi0SzFaE=
|
||||
github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
|
@ -330,7 +312,6 @@ github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk
|
|||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
|
@ -357,7 +338,6 @@ github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVo
|
|||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
|
||||
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
|
||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
|
@ -399,14 +379,12 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
|
|||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
|
@ -417,9 +395,7 @@ github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
|
|||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
|
@ -434,10 +410,8 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
|||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
|
@ -452,9 +426,7 @@ github.com/lestrrat-go/jwx v1.0.8 h1:Mj/2Ey9rkGx4w5IMQ2Q+9KLZn4cZoMgKrnMxi9eXE3k
|
|||
github.com/lestrrat-go/jwx v1.0.8/go.mod h1:6XJ5sxHF5U116AxYxeHfTnfsZRMgmeKY214zwZDdvho=
|
||||
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35 h1:lea8Wt+1ePkVrI2/WD+NgQT5r/XsLAzxeqtyFLcEs10=
|
||||
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/pdebug v0.0.0-20200204225717-4d6bd78da58d h1:aEZT3f1GGg5RIlHMAy4/4fe4ciOi3SCwYoaURphcB4k=
|
||||
github.com/lestrrat-go/pdebug v0.0.0-20200204225717-4d6bd78da58d/go.mod h1:B06CSso/AWxiPejj+fheUINGeBKeeEZNt8w+EoU7+L8=
|
||||
github.com/lestrrat-go/pdebug/v3 v3.0.0-20210111091911-ec4f5c88c087 h1:T5Wh8C/p5nWoGuEUBQj+daEXkj1CScB9GshvvsBJhpg=
|
||||
github.com/lestrrat-go/pdebug/v3 v3.0.0-20210111091911-ec4f5c88c087/go.mod h1:za+m+Ve24yCxTEhR59N7UlnJomWwCiIqbJRmKeiADU4=
|
||||
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
|
||||
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
|
@ -487,7 +459,6 @@ github.com/minio/sio v0.2.1/go.mod h1:8b0yPp2avGThviy/+OCJBI6OMpvxoUuiLvE6F1lebh
|
|||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
|
@ -514,7 +485,6 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE
|
|||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
|
@ -535,10 +505,8 @@ github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh
|
|||
github.com/otiai10/copy v1.4.2 h1:RTiz2sol3eoXPLF4o+YWqEybwfUa/Q2Nkc4ZIUs3fwI=
|
||||
github.com/otiai10/copy v1.4.2/go.mod h1:XWfuS3CrI0R6IE0FbgHsEazaXO8G0LpMp9o8tos0x4E=
|
||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||
github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI=
|
||||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||
github.com/otiai10/mint v1.3.2 h1:VYWnrP5fXmz1MXvjuUvcBrXSjGE6xjON+axB/UrpO3E=
|
||||
github.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
|
@ -613,7 +581,6 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB
|
|||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
|
||||
github.com/shirou/gopsutil/v3 v3.20.12 h1:abpcjSQRHdb3thCge/UyJty9CnvvmUHljTSrjtFU+Og=
|
||||
github.com/shirou/gopsutil/v3 v3.20.12/go.mod h1:igHnfak0qnw1biGeI2qKQvu0ZkwvEkUcCLlYhZzdr/4=
|
||||
|
@ -621,9 +588,7 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
|
|||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
|
@ -728,7 +693,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
|
|||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
|
@ -739,7 +703,6 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
|
|||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -760,7 +723,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -819,7 +781,6 @@ golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY=
|
||||
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -898,7 +859,6 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f
|
|||
golang.org/x/tools v0.0.0-20201202200335-bef1c476418a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201203202102-a1a1cbeaa516/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210115202250-e0d201561e39 h1:BTs2GMGSMWpgtCpv1CE7vkJTv7XcHdcLLnAMu7UbgTY=
|
||||
golang.org/x/tools v0.0.0-20210115202250-e0d201561e39/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -937,7 +897,6 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww
|
|||
google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
|
@ -1022,7 +981,6 @@ gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUy
|
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
|
|
|
@ -45,8 +45,8 @@ func getFolders(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
}
|
||||
if _, ok := r.URL.Query()["folder_path"]; ok {
|
||||
folderPath = r.URL.Query().Get("folder_path")
|
||||
if _, ok := r.URL.Query()["folder-path"]; ok {
|
||||
folderPath = r.URL.Query().Get("folder-path")
|
||||
}
|
||||
folders, err := dataprovider.GetFolders(limit, offset, order, folderPath)
|
||||
if err == nil {
|
||||
|
@ -84,8 +84,8 @@ func renderFolder(w http.ResponseWriter, r *http.Request, mappedPath string) {
|
|||
|
||||
func deleteFolderByPath(w http.ResponseWriter, r *http.Request) {
|
||||
var folderPath string
|
||||
if _, ok := r.URL.Query()["folder_path"]; ok {
|
||||
folderPath = r.URL.Query().Get("folder_path")
|
||||
if _, ok := r.URL.Query()["folder-path"]; ok {
|
||||
folderPath = r.URL.Query().Get("folder-path")
|
||||
}
|
||||
if folderPath == "" {
|
||||
err := errors.New("a non-empty folder path is required")
|
||||
|
|
|
@ -11,40 +11,56 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/drakkan/sftpgo/common"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
)
|
||||
|
||||
func validateBackupFile(outputFile string) (string, error) {
|
||||
if outputFile == "" {
|
||||
return "", errors.New("Invalid or missing output-file")
|
||||
}
|
||||
if filepath.IsAbs(outputFile) {
|
||||
return "", fmt.Errorf("Invalid output-file %#v: it must be a relative path", outputFile)
|
||||
}
|
||||
if strings.Contains(outputFile, "..") {
|
||||
return "", fmt.Errorf("Invalid output-file %#v", outputFile)
|
||||
}
|
||||
outputFile = filepath.Join(backupsPath, outputFile)
|
||||
return outputFile, nil
|
||||
}
|
||||
|
||||
func dumpData(w http.ResponseWriter, r *http.Request) {
|
||||
var outputFile, indent string
|
||||
if _, ok := r.URL.Query()["output_file"]; ok {
|
||||
outputFile = strings.TrimSpace(r.URL.Query().Get("output_file"))
|
||||
var outputFile, outputData, indent string
|
||||
if _, ok := r.URL.Query()["output-file"]; ok {
|
||||
outputFile = strings.TrimSpace(r.URL.Query().Get("output-file"))
|
||||
}
|
||||
if _, ok := r.URL.Query()["output-data"]; ok {
|
||||
outputData = strings.TrimSpace(r.URL.Query().Get("output-data"))
|
||||
}
|
||||
if _, ok := r.URL.Query()["indent"]; ok {
|
||||
indent = strings.TrimSpace(r.URL.Query().Get("indent"))
|
||||
}
|
||||
if outputFile == "" {
|
||||
sendAPIResponse(w, r, errors.New("Invalid or missing output_file"), "", http.StatusBadRequest)
|
||||
return
|
||||
|
||||
if outputData != "1" {
|
||||
var err error
|
||||
outputFile, err = validateBackupFile(outputFile)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = os.MkdirAll(filepath.Dir(outputFile), 0700)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "dumping data error: %v, output file: %#v", err, outputFile)
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
logger.Debug(logSender, "", "dumping data to: %#v", outputFile)
|
||||
}
|
||||
if filepath.IsAbs(outputFile) {
|
||||
sendAPIResponse(w, r, fmt.Errorf("Invalid output_file %#v: it must be a relative path", outputFile), "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if strings.Contains(outputFile, "..") {
|
||||
sendAPIResponse(w, r, fmt.Errorf("Invalid output_file %#v", outputFile), "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
outputFile = filepath.Join(backupsPath, outputFile)
|
||||
err := os.MkdirAll(filepath.Dir(outputFile), 0700)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, "", "dumping data error: %v, output file: %#v", err, outputFile)
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
logger.Debug(logSender, "", "dumping data to: %#v", outputFile)
|
||||
|
||||
backup, err := dataprovider.DumpData()
|
||||
if err != nil {
|
||||
|
@ -52,6 +68,13 @@ func dumpData(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
|
||||
if outputData == "1" {
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\"sftpgo-backup.json\"")
|
||||
render.JSON(w, r, backup)
|
||||
return
|
||||
}
|
||||
|
||||
var dump []byte
|
||||
if indent == "1" {
|
||||
dump, err = json.MarshalIndent(backup, "", " ")
|
||||
|
@ -70,6 +93,28 @@ func dumpData(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "Data saved", http.StatusOK)
|
||||
}
|
||||
|
||||
func loadDataFromRequest(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, MaxRestoreSize)
|
||||
_, scanQuota, mode, err := getLoaddataOptions(r)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil || len(content) == 0 {
|
||||
if len(content) == 0 {
|
||||
err = dataprovider.NewValidationError("request body is required")
|
||||
}
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
if err := restoreBackup(content, "", scanQuota, mode); err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
}
|
||||
sendAPIResponse(w, r, err, "Data restored", http.StatusOK)
|
||||
}
|
||||
|
||||
func loadData(w http.ResponseWriter, r *http.Request) {
|
||||
inputFile, scanQuota, mode, err := getLoaddataOptions(r)
|
||||
if err != nil {
|
||||
|
@ -96,30 +141,34 @@ func loadData(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
if err := restoreBackup(content, inputFile, scanQuota, mode); err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
}
|
||||
sendAPIResponse(w, r, err, "Data restored", http.StatusOK)
|
||||
}
|
||||
|
||||
func restoreBackup(content []byte, inputFile string, scanQuota, mode int) error {
|
||||
dump, err := dataprovider.ParseDumpData(content)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to parse input file: %#v", inputFile), http.StatusBadRequest)
|
||||
return
|
||||
return dataprovider.NewValidationError(fmt.Sprintf("Unable to parse backup content: %v", err))
|
||||
}
|
||||
|
||||
if err = RestoreFolders(dump.Folders, inputFile, scanQuota); err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if err = RestoreUsers(dump.Users, inputFile, mode, scanQuota); err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if err = RestoreAdmins(dump.Admins, inputFile, mode); err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debug(logSender, "", "backup restored, users: %v, folders: %v, admins: %vs",
|
||||
len(dump.Users), len(dump.Folders), len(dump.Admins))
|
||||
sendAPIResponse(w, r, err, "Data restored", http.StatusOK)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLoaddataOptions(r *http.Request) (string, int, int, error) {
|
||||
|
@ -127,19 +176,21 @@ func getLoaddataOptions(r *http.Request) (string, int, int, error) {
|
|||
var err error
|
||||
scanQuota := 0
|
||||
restoreMode := 0
|
||||
if _, ok := r.URL.Query()["input_file"]; ok {
|
||||
inputFile = strings.TrimSpace(r.URL.Query().Get("input_file"))
|
||||
if _, ok := r.URL.Query()["input-file"]; ok {
|
||||
inputFile = strings.TrimSpace(r.URL.Query().Get("input-file"))
|
||||
}
|
||||
if _, ok := r.URL.Query()["scan_quota"]; ok {
|
||||
scanQuota, err = strconv.Atoi(r.URL.Query().Get("scan_quota"))
|
||||
if _, ok := r.URL.Query()["scan-quota"]; ok {
|
||||
scanQuota, err = strconv.Atoi(r.URL.Query().Get("scan-quota"))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("invalid scan_quota: %v", err)
|
||||
return inputFile, scanQuota, restoreMode, err
|
||||
}
|
||||
}
|
||||
if _, ok := r.URL.Query()["mode"]; ok {
|
||||
restoreMode, err = strconv.Atoi(r.URL.Query().Get("mode"))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("invalid mode: %v", err)
|
||||
return inputFile, scanQuota, restoreMode, err
|
||||
}
|
||||
}
|
||||
return inputFile, scanQuota, restoreMode, err
|
||||
|
|
|
@ -54,6 +54,9 @@ const (
|
|||
webStatusPath = "/web/status"
|
||||
webAdminsPath = "/web/admins"
|
||||
webAdminPath = "/web/admin"
|
||||
webMaintenancePath = "/web/maintenance"
|
||||
webBackupPath = "/web/backup"
|
||||
webRestorePath = "/web/restore"
|
||||
webScanVFolderPath = "/web/folder-quota-scans"
|
||||
webQuotaScanPath = "/web/quota-scans"
|
||||
webChangeAdminPwdPath = "/web/changepwd/admin"
|
||||
|
|
|
@ -73,6 +73,8 @@ const (
|
|||
webStatusPath = "/web/status"
|
||||
webAdminsPath = "/web/admins"
|
||||
webAdminPath = "/web/admin"
|
||||
webMaintenancePath = "/web/maintenance"
|
||||
webRestorePath = "/web/restore"
|
||||
webChangeAdminPwdPath = "/web/changepwd/admin"
|
||||
httpBaseURL = "http://127.0.0.1:8081"
|
||||
configDir = ".."
|
||||
|
@ -2179,7 +2181,7 @@ func TestProviderErrors(t *testing.T) {
|
|||
if assert.NoError(t, err) {
|
||||
assert.False(t, status.DataProvider.IsActive)
|
||||
}
|
||||
_, _, err = httpdtest.Dumpdata("backup.json", "", http.StatusInternalServerError)
|
||||
_, _, err = httpdtest.Dumpdata("backup.json", "", "", http.StatusInternalServerError)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = httpdtest.GetFolders(0, 0, "", http.StatusInternalServerError)
|
||||
assert.NoError(t, err)
|
||||
|
@ -2296,25 +2298,35 @@ func TestDumpdata(t *testing.T) {
|
|||
providerConf := config.GetProviderConf()
|
||||
err = dataprovider.Initialize(providerConf, configDir, true)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = httpdtest.Dumpdata("", "", http.StatusBadRequest)
|
||||
_, _, err = httpdtest.Dumpdata("", "", "", http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = httpdtest.Dumpdata(filepath.Join(backupsPath, "backup.json"), "", http.StatusBadRequest)
|
||||
_, _, err = httpdtest.Dumpdata(filepath.Join(backupsPath, "backup.json"), "", "", http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = httpdtest.Dumpdata("../backup.json", "", http.StatusBadRequest)
|
||||
_, _, err = httpdtest.Dumpdata("../backup.json", "", "", http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = httpdtest.Dumpdata("backup.json", "0", http.StatusOK)
|
||||
_, _, err = httpdtest.Dumpdata("backup.json", "", "0", http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = httpdtest.Dumpdata("backup.json", "1", http.StatusOK)
|
||||
response, _, err := httpdtest.Dumpdata("", "1", "0", http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, ok := response["admins"]
|
||||
assert.True(t, ok)
|
||||
_, ok = response["users"]
|
||||
assert.True(t, ok)
|
||||
_, ok = response["folders"]
|
||||
assert.True(t, ok)
|
||||
_, ok = response["version"]
|
||||
assert.True(t, ok)
|
||||
_, _, err = httpdtest.Dumpdata("backup.json", "", "1", http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(filepath.Join(backupsPath, "backup.json"))
|
||||
assert.NoError(t, err)
|
||||
if runtime.GOOS != "windows" {
|
||||
err = os.Chmod(backupsPath, 0001)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = httpdtest.Dumpdata("bck.json", "", http.StatusInternalServerError)
|
||||
_, _, err = httpdtest.Dumpdata("bck.json", "", "", http.StatusInternalServerError)
|
||||
assert.NoError(t, err)
|
||||
// subdir cannot be created
|
||||
_, _, err = httpdtest.Dumpdata(filepath.Join("subdir", "bck.json"), "", http.StatusInternalServerError)
|
||||
_, _, err = httpdtest.Dumpdata(filepath.Join("subdir", "bck.json"), "", "", http.StatusInternalServerError)
|
||||
assert.NoError(t, err)
|
||||
err = os.Chmod(backupsPath, 0755)
|
||||
assert.NoError(t, err)
|
||||
|
@ -2396,6 +2408,63 @@ func TestDefenderAPIErrors(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLoaddataFromPostBody(t *testing.T) {
|
||||
mappedPath := filepath.Join(os.TempDir(), "restored_folder")
|
||||
user := getTestUser()
|
||||
user.ID = 1
|
||||
user.Username = "test_user_restored"
|
||||
admin := getTestAdmin()
|
||||
admin.ID = 1
|
||||
admin.Username = "test_admin_restored"
|
||||
backupData := dataprovider.BackupData{}
|
||||
backupData.Users = append(backupData.Users, user)
|
||||
backupData.Admins = append(backupData.Admins, admin)
|
||||
backupData.Folders = []vfs.BaseVirtualFolder{
|
||||
{
|
||||
MappedPath: mappedPath,
|
||||
UsedQuotaSize: 123,
|
||||
UsedQuotaFiles: 456,
|
||||
LastQuotaUpdate: 789,
|
||||
Users: []string{"user"},
|
||||
},
|
||||
{
|
||||
MappedPath: mappedPath,
|
||||
},
|
||||
}
|
||||
backupContent, err := json.Marshal(backupData)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = httpdtest.LoaddataFromPostBody(nil, "0", "0", http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = httpdtest.LoaddataFromPostBody(backupContent, "a", "0", http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = httpdtest.LoaddataFromPostBody([]byte("invalid content"), "0", "0", http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = httpdtest.LoaddataFromPostBody(backupContent, "0", "0", http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
folders, _, err := httpdtest.GetFolders(1, 0, mappedPath, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, folders, 1) {
|
||||
folder := folders[0]
|
||||
assert.Equal(t, mappedPath, folder.MappedPath)
|
||||
assert.Equal(t, int64(123), folder.UsedQuotaSize)
|
||||
assert.Equal(t, 456, folder.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(789), folder.LastQuotaUpdate)
|
||||
assert.Len(t, folder.Users, 0)
|
||||
_, err = httpdtest.RemoveFolder(folder, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoaddata(t *testing.T) {
|
||||
mappedPath := filepath.Join(os.TempDir(), "restored_folder")
|
||||
user := getTestUser()
|
||||
|
@ -3269,7 +3338,7 @@ func TestUpdateFolderQuotaUsageMock(t *testing.T) {
|
|||
url, err := url.Parse(folderPath)
|
||||
assert.NoError(t, err)
|
||||
q := url.Query()
|
||||
q.Add("folder_path", mappedPath)
|
||||
q.Add("folder-path", mappedPath)
|
||||
url.RawQuery = q.Encode()
|
||||
req, _ = http.NewRequest(http.MethodGet, url.String(), nil)
|
||||
setBearerForReq(req, token)
|
||||
|
@ -3298,7 +3367,7 @@ func TestUpdateFolderQuotaUsageMock(t *testing.T) {
|
|||
url, err = url.Parse(folderPath)
|
||||
assert.NoError(t, err)
|
||||
q = url.Query()
|
||||
q.Add("folder_path", mappedPath)
|
||||
q.Add("folder-path", mappedPath)
|
||||
url.RawQuery = q.Encode()
|
||||
req, _ = http.NewRequest(http.MethodDelete, url.String(), nil)
|
||||
setBearerForReq(req, token)
|
||||
|
@ -3360,7 +3429,7 @@ func TestStartFolderQuotaScanMock(t *testing.T) {
|
|||
url, err := url.Parse(folderPath)
|
||||
assert.NoError(t, err)
|
||||
q := url.Query()
|
||||
q.Add("folder_path", mappedPath)
|
||||
q.Add("folder-path", mappedPath)
|
||||
url.RawQuery = q.Encode()
|
||||
req, _ = http.NewRequest(http.MethodDelete, url.String(), nil)
|
||||
setBearerForReq(req, token)
|
||||
|
@ -3435,7 +3504,7 @@ func TestGetFoldersMock(t *testing.T) {
|
|||
url, err := url.Parse(folderPath + "?limit=510&offset=0&order=DESC")
|
||||
assert.NoError(t, err)
|
||||
q := url.Query()
|
||||
q.Add("folder_path", mappedPath)
|
||||
q.Add("folder-path", mappedPath)
|
||||
url.RawQuery = q.Encode()
|
||||
req, _ = http.NewRequest(http.MethodGet, url.String(), nil)
|
||||
setBearerForReq(req, token)
|
||||
|
@ -3460,7 +3529,7 @@ func TestGetFoldersMock(t *testing.T) {
|
|||
url, err = url.Parse(folderPath)
|
||||
assert.NoError(t, err)
|
||||
q = url.Query()
|
||||
q.Add("folder_path", mappedPath)
|
||||
q.Add("folder-path", mappedPath)
|
||||
url.RawQuery = q.Encode()
|
||||
req, _ = http.NewRequest(http.MethodDelete, url.String(), nil)
|
||||
setBearerForReq(req, token)
|
||||
|
@ -3974,6 +4043,100 @@ func TestAdminUpdateSelfMock(t *testing.T) {
|
|||
assert.Contains(t, rr.Body.String(), "You cannot disable yourself")
|
||||
}
|
||||
|
||||
func TestWebMaintenanceMock(t *testing.T) {
|
||||
token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
req, _ := http.NewRequest(http.MethodGet, webMaintenancePath, nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
form := make(url.Values)
|
||||
form.Set("mode", "a")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webRestorePath, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
form.Set("mode", "0")
|
||||
form.Set("quota", "a")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webRestorePath, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
form.Set("quota", "0")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webRestorePath, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
req, _ = http.NewRequest(http.MethodPost, webRestorePath+"?a=%3", &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
backupFilePath := filepath.Join(os.TempDir(), "backup.json")
|
||||
err = createTestFile(backupFilePath, 0)
|
||||
assert.NoError(t, err)
|
||||
|
||||
b, contentType, _ = getMultipartFormData(form, "backup_file", backupFilePath)
|
||||
req, _ = http.NewRequest(http.MethodPost, webRestorePath, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
err = createTestFile(backupFilePath, 10)
|
||||
assert.NoError(t, err)
|
||||
|
||||
b, contentType, _ = getMultipartFormData(form, "backup_file", backupFilePath)
|
||||
req, _ = http.NewRequest(http.MethodPost, webRestorePath, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
user := getTestUser()
|
||||
user.ID = 1
|
||||
user.Username = "test_user_web_restore"
|
||||
admin := getTestAdmin()
|
||||
admin.ID = 1
|
||||
admin.Username = "test_admin_web_restore"
|
||||
backupData := dataprovider.BackupData{}
|
||||
backupData.Users = append(backupData.Users, user)
|
||||
backupData.Admins = append(backupData.Admins, admin)
|
||||
backupContent, err := json.Marshal(backupData)
|
||||
assert.NoError(t, err)
|
||||
err = ioutil.WriteFile(backupFilePath, backupContent, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
b, contentType, _ = getMultipartFormData(form, "backup_file", backupFilePath)
|
||||
req, _ = http.NewRequest(http.MethodPost, webRestorePath, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Your backup was successfully restored")
|
||||
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWebUserAddMock(t *testing.T) {
|
||||
token, err := getJWTTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
|
@ -4190,7 +4353,7 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
url, err := url.Parse(folderPath)
|
||||
assert.NoError(t, err)
|
||||
q := url.Query()
|
||||
q.Add("folder_path", mappedDir)
|
||||
q.Add("folder-path", mappedDir)
|
||||
url.RawQuery = q.Encode()
|
||||
req, _ = http.NewRequest(http.MethodDelete, url.String(), nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
|
@ -4904,7 +5067,7 @@ func TestAddWebFoldersMock(t *testing.T) {
|
|||
url, err := url.Parse(folderPath)
|
||||
assert.NoError(t, err)
|
||||
q := url.Query()
|
||||
q.Add("folder_path", mappedPath)
|
||||
q.Add("folder-path", mappedPath)
|
||||
url.RawQuery = q.Encode()
|
||||
req, _ = http.NewRequest(http.MethodGet, url.String(), nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
|
@ -4920,7 +5083,7 @@ func TestAddWebFoldersMock(t *testing.T) {
|
|||
url, err = url.Parse(folderPath)
|
||||
assert.NoError(t, err)
|
||||
q = url.Query()
|
||||
q.Add("folder_path", mappedPath)
|
||||
q.Add("folder-path", mappedPath)
|
||||
url.RawQuery = q.Encode()
|
||||
req, _ = http.NewRequest(http.MethodDelete, url.String(), nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
|
@ -4970,7 +5133,7 @@ func TestWebFoldersMock(t *testing.T) {
|
|||
url, err := url.Parse(folderPath)
|
||||
assert.NoError(t, err)
|
||||
q := url.Query()
|
||||
q.Add("folder_path", folder.MappedPath)
|
||||
q.Add("folder-path", folder.MappedPath)
|
||||
url.RawQuery = q.Encode()
|
||||
req, _ := http.NewRequest(http.MethodDelete, url.String(), nil)
|
||||
setJWTCookieForReq(req, token)
|
||||
|
|
|
@ -2,7 +2,7 @@ openapi: 3.0.3
|
|||
info:
|
||||
title: SFTPGo
|
||||
description: SFTPGo REST API
|
||||
version: 2.4.1
|
||||
version: 2.4.2
|
||||
|
||||
servers:
|
||||
- url: /api/v2
|
||||
|
@ -497,7 +497,7 @@ paths:
|
|||
- DESC
|
||||
example: ASC
|
||||
- in: query
|
||||
name: folder_path
|
||||
name: folder-path
|
||||
required: false
|
||||
description: Filter by folder path, extact match case sensitive
|
||||
schema:
|
||||
|
@ -556,7 +556,7 @@ paths:
|
|||
summary: Delete an existing folder
|
||||
operationId: delete_folder
|
||||
parameters:
|
||||
- name: folder_path
|
||||
- name: folder-path
|
||||
in: query
|
||||
description: path to the folder to delete
|
||||
required: true
|
||||
|
@ -994,15 +994,25 @@ paths:
|
|||
tags:
|
||||
- maintenance
|
||||
summary: Backup SFTPGo data as data provider independent JSON
|
||||
description: The backup is saved to a local file to avoid to expose sensitive data over the network. The output of dumpdata can be used as input for loaddata
|
||||
description: The backup can be saved in a local file on the server, to avoid exposing sensitive data over the network, or returned as response body. The output of dumpdata can be used as input for loaddata
|
||||
operationId: dumpdata
|
||||
parameters:
|
||||
- in: query
|
||||
name: output_file
|
||||
name: output-file
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: Path for the file to write the JSON serialized data to. This path is relative to the configured "backups_path". If this file already exists it will be overwritten
|
||||
description: Path for the file to write the JSON serialized data to. This path is relative to the configured "backups_path". If this file already exists it will be overwritten. To return the backup as response body set `output_data` to true instead.
|
||||
- in: query
|
||||
name: output-data
|
||||
schema:
|
||||
type: integer
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
description: >
|
||||
output_data:
|
||||
* `0` or any other value != 1, the backup will be saved to a file on the server, `output_file` is required
|
||||
* `1` the backup will be returned as response body
|
||||
- in: query
|
||||
name: indent
|
||||
schema:
|
||||
|
@ -1020,9 +1030,9 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
message: "Data saved"
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/ApiResponse'
|
||||
- $ref: '#/components/schemas/BackupData'
|
||||
400:
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
401:
|
||||
|
@ -1034,46 +1044,78 @@ paths:
|
|||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/loaddata:
|
||||
parameters:
|
||||
- in: query
|
||||
name: scan-quota
|
||||
schema:
|
||||
type: integer
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
description: >
|
||||
Quota scan:
|
||||
* `0` no quota scan is done, the imported users/folders will have used_quota_size and used_quota_files = 0 or the existing values if they already exists. This is the default
|
||||
* `1` scan quota
|
||||
* `2` scan quota if the user has quota restrictions
|
||||
required: false
|
||||
- in: query
|
||||
name: mode
|
||||
schema:
|
||||
type: integer
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
description: >
|
||||
Mode:
|
||||
* `0` New users/admins are added, existing users/admins are updated. This is the default
|
||||
* `1` New users/admins are added, existing users/admins are not modified
|
||||
* `2` New users are added, existing users are updated and, if connected, they are disconnected and so forced to use the new configuration
|
||||
get:
|
||||
tags:
|
||||
- maintenance
|
||||
summary: Restore SFTPGo data from a JSON backup
|
||||
description: Users and folders will be restored one by one and the restore is stopped if a user/folder cannot be added or updated, so it could happen a partial restore
|
||||
operationId: loaddata
|
||||
summary: Restore SFTPGo data from a JSON backup file on the server
|
||||
description: Users, folders and admins will be restored one by one and the restore is stopped if a user/folder/admin cannot be added or updated, so it could happen a partial restore
|
||||
operationId: loaddata_from_file
|
||||
parameters:
|
||||
- in: query
|
||||
name: input_file
|
||||
name: input-file
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: Path for the file to read the JSON serialized data from. This can be an absolute path or a path relative to the configured "backups_path". The max allowed file size is 10MB
|
||||
- in: query
|
||||
name: scan_quota
|
||||
schema:
|
||||
type: integer
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
description: >
|
||||
Quota scan:
|
||||
* `0` no quota scan is done, the imported users will have used_quota_size and used_quota_files = 0 or the existing values if they already exists. This is the default
|
||||
* `1` scan quota
|
||||
* `2` scan quota if the user has quota restrictions
|
||||
required: false
|
||||
- in: query
|
||||
name: mode
|
||||
schema:
|
||||
type: integer
|
||||
enum:
|
||||
- 0
|
||||
- 1
|
||||
- 2
|
||||
description: >
|
||||
Mode:
|
||||
* `0` New users are added, existing users are updated. This is the default
|
||||
* `1` New users are added, existing users are not modified
|
||||
* `2` New users are added, existing users are updated and, if connected, they are disconnected and so forced to use the new configuration
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/ApiResponse'
|
||||
example:
|
||||
message: "Data restored"
|
||||
400:
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
post:
|
||||
tags:
|
||||
- maintenance
|
||||
summary: Restore SFTPGo data from a JSON backup
|
||||
description: Users, folders and admins will be restored one by one and the restore is stopped if a user/folder/admin cannot be added or updated, so it could happen a partial restore
|
||||
operationId: loaddata_from_request_body
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref : '#/components/schemas/BackupData'
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
|
@ -1873,6 +1915,23 @@ components:
|
|||
score:
|
||||
type: integer
|
||||
description: if 0 the host is not listed
|
||||
BackupData:
|
||||
type: object
|
||||
properties:
|
||||
users:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/User'
|
||||
folders:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/BaseVirtualFolder'
|
||||
admins:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Admin'
|
||||
version:
|
||||
type: integer
|
||||
PwdChange:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
@ -302,6 +302,7 @@ func (s *httpdServer) initializeRouter() {
|
|||
router.With(checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(folderPath, deleteFolderByPath)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(dumpDataPath, dumpData)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(loadDataPath, loadData)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(loadDataPath, loadDataFromRequest)
|
||||
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(updateUsedQuotaPath, updateUserQuotaUsage)
|
||||
router.With(checkPerm(dataprovider.PermAdminChangeUsers)).Put(updateFolderUsedQuotaPath, updateVFolderQuotaUsage)
|
||||
router.With(checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderBanTime, getBanTime)
|
||||
|
@ -365,6 +366,9 @@ func (s *httpdServer) initializeRouter() {
|
|||
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(webScanVFolderPath, startVFolderQuotaScan)
|
||||
router.With(checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(webUserPath+"/{username}", deleteUser)
|
||||
router.With(checkPerm(dataprovider.PermAdminQuotaScans)).Post(webQuotaScanPath, startQuotaScan)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(webMaintenancePath, handleWebMaintenance)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Get(webBackupPath, dumpData)
|
||||
router.With(checkPerm(dataprovider.PermAdminManageSystem)).Post(webRestorePath, handleWebRestore)
|
||||
})
|
||||
|
||||
router.Group(func(router chi.Router) {
|
||||
|
|
74
httpd/web.go
74
httpd/web.go
|
@ -34,12 +34,14 @@ const (
|
|||
templateStatus = "status.html"
|
||||
templateLogin = "login.html"
|
||||
templateChangePwd = "changepwd.html"
|
||||
templateMaintenance = "maintenance.html"
|
||||
pageUsersTitle = "Users"
|
||||
pageAdminsTitle = "Admins"
|
||||
pageConnectionsTitle = "Connections"
|
||||
pageStatusTitle = "Status"
|
||||
pageFoldersTitle = "Folders"
|
||||
pageChangePwdTitle = "Change password"
|
||||
pageMaintenanceTitle = "Maintenance"
|
||||
page400Title = "Bad request"
|
||||
page403Title = "Forbidden"
|
||||
page404Title = "Not found"
|
||||
|
@ -70,11 +72,13 @@ type basePage struct {
|
|||
ChangeAdminPwdURL string
|
||||
FolderQuotaScanURL string
|
||||
StatusURL string
|
||||
MaintenanceURL string
|
||||
UsersTitle string
|
||||
AdminsTitle string
|
||||
ConnectionsTitle string
|
||||
FoldersTitle string
|
||||
StatusTitle string
|
||||
MaintenanceTitle string
|
||||
Version string
|
||||
LoggedAdmin *dataprovider.Admin
|
||||
}
|
||||
|
@ -129,6 +133,13 @@ type changePwdPage struct {
|
|||
Error string
|
||||
}
|
||||
|
||||
type maintenancePage struct {
|
||||
basePage
|
||||
BackupPath string
|
||||
RestorePath string
|
||||
Error string
|
||||
}
|
||||
|
||||
type folderPage struct {
|
||||
basePage
|
||||
Folder vfs.BaseVirtualFolder
|
||||
|
@ -191,6 +202,10 @@ func loadTemplates(templatesPath string) {
|
|||
loginPath := []string{
|
||||
filepath.Join(templatesPath, templateLogin),
|
||||
}
|
||||
maintenancePath := []string{
|
||||
filepath.Join(templatesPath, templateBase),
|
||||
filepath.Join(templatesPath, templateMaintenance),
|
||||
}
|
||||
usersTmpl := utils.LoadTemplate(template.ParseFiles(usersPaths...))
|
||||
userTmpl := utils.LoadTemplate(template.ParseFiles(userPaths...))
|
||||
adminsTmpl := utils.LoadTemplate(template.ParseFiles(adminsPaths...))
|
||||
|
@ -202,6 +217,7 @@ func loadTemplates(templatesPath string) {
|
|||
statusTmpl := utils.LoadTemplate(template.ParseFiles(statusPath...))
|
||||
loginTmpl := utils.LoadTemplate(template.ParseFiles(loginPath...))
|
||||
changePwdTmpl := utils.LoadTemplate(template.ParseFiles(changePwdPaths...))
|
||||
maintenanceTmpl := utils.LoadTemplate(template.ParseFiles(maintenancePath...))
|
||||
|
||||
templates[templateUsers] = usersTmpl
|
||||
templates[templateUser] = userTmpl
|
||||
|
@ -214,6 +230,7 @@ func loadTemplates(templatesPath string) {
|
|||
templates[templateStatus] = statusTmpl
|
||||
templates[templateLogin] = loginTmpl
|
||||
templates[templateChangePwd] = changePwdTmpl
|
||||
templates[templateMaintenance] = maintenanceTmpl
|
||||
}
|
||||
|
||||
func getBasePageData(title, currentURL string, r *http.Request) basePage {
|
||||
|
@ -232,11 +249,13 @@ func getBasePageData(title, currentURL string, r *http.Request) basePage {
|
|||
ConnectionsURL: webConnectionsPath,
|
||||
StatusURL: webStatusPath,
|
||||
FolderQuotaScanURL: webScanVFolderPath,
|
||||
MaintenanceURL: webMaintenancePath,
|
||||
UsersTitle: pageUsersTitle,
|
||||
AdminsTitle: pageAdminsTitle,
|
||||
ConnectionsTitle: pageConnectionsTitle,
|
||||
FoldersTitle: pageFoldersTitle,
|
||||
StatusTitle: pageStatusTitle,
|
||||
MaintenanceTitle: pageMaintenanceTitle,
|
||||
Version: version.GetAsString(),
|
||||
LoggedAdmin: getAdminFromToken(r),
|
||||
}
|
||||
|
@ -291,6 +310,17 @@ func renderChangePwdPage(w http.ResponseWriter, r *http.Request, error string) {
|
|||
renderTemplate(w, templateChangePwd, data)
|
||||
}
|
||||
|
||||
func renderMaintenancePage(w http.ResponseWriter, r *http.Request, error string) {
|
||||
data := maintenancePage{
|
||||
basePage: getBasePageData(pageMaintenanceTitle, webMaintenancePath, r),
|
||||
BackupPath: webBackupPath,
|
||||
RestorePath: webRestorePath,
|
||||
Error: error,
|
||||
}
|
||||
|
||||
renderTemplate(w, templateMaintenance, data)
|
||||
}
|
||||
|
||||
func renderAddUpdateAdminPage(w http.ResponseWriter, r *http.Request, admin *dataprovider.Admin,
|
||||
error string, isAdd bool) {
|
||||
currentURL := webAdminPath
|
||||
|
@ -796,6 +826,50 @@ func handleWebLogin(w http.ResponseWriter, r *http.Request) {
|
|||
renderLoginPage(w, "")
|
||||
}
|
||||
|
||||
func handleWebMaintenance(w http.ResponseWriter, r *http.Request) {
|
||||
renderMaintenancePage(w, r, "")
|
||||
}
|
||||
|
||||
func handleWebRestore(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseMultipartForm(MaxRestoreSize)
|
||||
if err != nil {
|
||||
renderMaintenancePage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
restoreMode, err := strconv.Atoi(r.Form.Get("mode"))
|
||||
if err != nil {
|
||||
renderMaintenancePage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
scanQuota, err := strconv.Atoi(r.Form.Get("quota"))
|
||||
if err != nil {
|
||||
renderMaintenancePage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
backupFile, _, err := r.FormFile("backup_file")
|
||||
if err != nil {
|
||||
renderMaintenancePage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
defer backupFile.Close()
|
||||
|
||||
backupContent, err := ioutil.ReadAll(backupFile)
|
||||
if err != nil || len(backupContent) == 0 {
|
||||
if len(backupContent) == 0 {
|
||||
err = errors.New("backup file size must be greater than 0")
|
||||
}
|
||||
renderMaintenancePage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := restoreBackup(backupContent, "", scanQuota, restoreMode); err != nil {
|
||||
renderMaintenancePage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
renderMessagePage(w, r, "Data restored", "", http.StatusOK, nil, "Your backup was successfully restored")
|
||||
}
|
||||
|
||||
func handleGetWebAdmins(w http.ResponseWriter, r *http.Request) {
|
||||
limit := defaultQueryLimit
|
||||
if _, ok := r.URL.Query()["qlimit"]; ok {
|
||||
|
|
|
@ -486,7 +486,7 @@ func RemoveFolder(folder vfs.BaseVirtualFolder, expectedStatusCode int) ([]byte,
|
|||
return body, err
|
||||
}
|
||||
q := url.Query()
|
||||
q.Add("folder_path", folder.MappedPath)
|
||||
q.Add("folder-path", folder.MappedPath)
|
||||
url.RawQuery = q.Encode()
|
||||
resp, err := sendHTTPRequest(http.MethodDelete, url.String(), nil, "", getDefaultToken())
|
||||
if err != nil {
|
||||
|
@ -510,7 +510,7 @@ func GetFolders(limit int64, offset int64, mappedPath string, expectedStatusCode
|
|||
}
|
||||
if len(mappedPath) > 0 {
|
||||
q := url.Query()
|
||||
q.Add("folder_path", mappedPath)
|
||||
q.Add("folder-path", mappedPath)
|
||||
url.RawQuery = q.Encode()
|
||||
}
|
||||
resp, err := sendHTTPRequest(http.MethodGet, url.String(), nil, "", getDefaultToken())
|
||||
|
@ -678,7 +678,7 @@ func UnbanIP(ip string, expectedStatusCode int) error {
|
|||
|
||||
// Dumpdata requests a backup to outputFile.
|
||||
// outputFile is relative to the configured backups_path
|
||||
func Dumpdata(outputFile, indent string, expectedStatusCode int) (map[string]interface{}, []byte, error) {
|
||||
func Dumpdata(outputFile, outputData, indent string, expectedStatusCode int) (map[string]interface{}, []byte, error) {
|
||||
var response map[string]interface{}
|
||||
var body []byte
|
||||
url, err := url.Parse(buildURLRelativeToBase(dumpDataPath))
|
||||
|
@ -686,8 +686,13 @@ func Dumpdata(outputFile, indent string, expectedStatusCode int) (map[string]int
|
|||
return response, body, err
|
||||
}
|
||||
q := url.Query()
|
||||
q.Add("output_file", outputFile)
|
||||
if len(indent) > 0 {
|
||||
if outputData != "" {
|
||||
q.Add("output-data", outputData)
|
||||
}
|
||||
if outputFile != "" {
|
||||
q.Add("output-file", outputFile)
|
||||
}
|
||||
if indent != "" {
|
||||
q.Add("indent", indent)
|
||||
}
|
||||
url.RawQuery = q.Encode()
|
||||
|
@ -706,8 +711,6 @@ func Dumpdata(outputFile, indent string, expectedStatusCode int) (map[string]int
|
|||
}
|
||||
|
||||
// Loaddata restores a backup.
|
||||
// New users are added, existing users are updated. Users will be restored one by one and the restore is stopped if a
|
||||
// user cannot be added/updated, so it could happen a partial restore
|
||||
func Loaddata(inputFile, scanQuota, mode string, expectedStatusCode int) (map[string]interface{}, []byte, error) {
|
||||
var response map[string]interface{}
|
||||
var body []byte
|
||||
|
@ -716,11 +719,11 @@ func Loaddata(inputFile, scanQuota, mode string, expectedStatusCode int) (map[st
|
|||
return response, body, err
|
||||
}
|
||||
q := url.Query()
|
||||
q.Add("input_file", inputFile)
|
||||
if len(scanQuota) > 0 {
|
||||
q.Add("scan_quota", scanQuota)
|
||||
q.Add("input-file", inputFile)
|
||||
if scanQuota != "" {
|
||||
q.Add("scan-quota", scanQuota)
|
||||
}
|
||||
if len(mode) > 0 {
|
||||
if mode != "" {
|
||||
q.Add("mode", mode)
|
||||
}
|
||||
url.RawQuery = q.Encode()
|
||||
|
@ -738,6 +741,36 @@ func Loaddata(inputFile, scanQuota, mode string, expectedStatusCode int) (map[st
|
|||
return response, body, err
|
||||
}
|
||||
|
||||
// LoaddataFromPostBody restores a backup
|
||||
func LoaddataFromPostBody(data []byte, scanQuota, mode string, expectedStatusCode int) (map[string]interface{}, []byte, error) {
|
||||
var response map[string]interface{}
|
||||
var body []byte
|
||||
url, err := url.Parse(buildURLRelativeToBase(loadDataPath))
|
||||
if err != nil {
|
||||
return response, body, err
|
||||
}
|
||||
q := url.Query()
|
||||
if scanQuota != "" {
|
||||
q.Add("scan-quota", scanQuota)
|
||||
}
|
||||
if mode != "" {
|
||||
q.Add("mode", mode)
|
||||
}
|
||||
url.RawQuery = q.Encode()
|
||||
resp, err := sendHTTPRequest(http.MethodPost, url.String(), bytes.NewReader(data), "", getDefaultToken())
|
||||
if err != nil {
|
||||
return response, body, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
err = checkResponse(resp.StatusCode, expectedStatusCode)
|
||||
if err == nil && expectedStatusCode == http.StatusOK {
|
||||
err = render.DecodeJSON(resp.Body, &response)
|
||||
} else {
|
||||
body, _ = getResponseBody(resp)
|
||||
}
|
||||
return response, body, err
|
||||
}
|
||||
|
||||
func checkResponse(actual int, expected int) error {
|
||||
if expected != actual {
|
||||
return fmt.Errorf("wrong status code: got %v want %v", actual, expected)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
NFPM_VERSION=2.2.2
|
||||
NFPM_VERSION=2.2.3
|
||||
NFPM_ARCH=${NFPM_ARCH:-amd64}
|
||||
if [ -z ${SFTPGO_VERSION} ]
|
||||
then
|
||||
|
|
|
@ -3,90 +3,93 @@
|
|||
{{define "title"}}{{.Title}}{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
<!-- Page Heading -->
|
||||
<h1 class="h5 mb-4 text-gray-800">{{if .IsAdd}}Add a new admin{{else}}Edit admin{{end}}</h1>
|
||||
{{if .Error}}
|
||||
<div class="card mb-4 border-left-warning">
|
||||
<div class="card-body text-form-error">{{.Error}}</div>
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">{{if .IsAdd}}Add a new admin{{else}}Edit admin{{end}}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .Error}}
|
||||
<div class="card mb-4 border-left-warning">
|
||||
<div class="card-body text-form-error">{{.Error}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<form id="admin_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="{{.Admin.Username}}" maxlength="255" autocomplete="nope" required {{if not .IsAdd}}readonly{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idStatus" class="col-sm-2 col-form-label">Status</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idStatus" name="status">
|
||||
<option value="1" {{if eq .Admin.Status 1 }}selected{{end}}>Active</option>
|
||||
<option value="0" {{if eq .Admin.Status 0 }}selected{{end}}>Inactive</option>
|
||||
</select>
|
||||
</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=""
|
||||
{{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="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 := .Admin.GetValidPerms}}
|
||||
<option value="{{$validPerm}}" {{range $perm :=$.Admin.Permissions }}
|
||||
{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idEmail" class="col-sm-2 col-form-label">Email</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idEmail" name="email" placeholder=""
|
||||
value="{{.Admin.Email}}" maxlength="255">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idAllowedIP" class="col-sm-2 col-form-label">Allowed IP/Mask</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idAllowedIP" name="allowed_ip" placeholder=""
|
||||
value="{{.Admin.GetAllowedIPAsString}}" maxlength="255" aria-describedby="allowedIPHelpBlock">
|
||||
<small id="allowedIPHelpBlock" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idAdditionalInfo" class="col-sm-2 col-form-label">Additional info</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idAdditionalInfo" name="additional_info" rows="3"
|
||||
aria-describedby="additionalInfoHelpBlock">{{.Admin.AdditionalInfo}}</textarea>
|
||||
<small id="additionalInfoHelpBlock" class="form-text text-muted">
|
||||
Free form text field
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<form id="admin_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="{{.Admin.Username}}" maxlength="255" autocomplete="nope" required
|
||||
{{if not .IsAdd}}readonly{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idStatus" class="col-sm-2 col-form-label">Status</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idStatus" name="status">
|
||||
<option value="1" {{if eq .Admin.Status 1 }}selected{{end}}>Active</option>
|
||||
<option value="0" {{if eq .Admin.Status 0 }}selected{{end}}>Inactive</option>
|
||||
</select>
|
||||
</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=""
|
||||
{{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="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 := .Admin.GetValidPerms}}
|
||||
<option value="{{$validPerm}}"
|
||||
{{range $perm := $.Admin.Permissions }}{{if eq $perm $validPerm}}selected{{end}}{{end}}>{{$validPerm}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idEmail" class="col-sm-2 col-form-label">Email</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idEmail" name="email" placeholder=""
|
||||
value="{{.Admin.Email}}" maxlength="255">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idAllowedIP" class="col-sm-2 col-form-label">Allowed IP/Mask</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idAllowedIP" name="allowed_ip" placeholder=""
|
||||
value="{{.Admin.GetAllowedIPAsString}}" maxlength="255" aria-describedby="allowedIPHelpBlock">
|
||||
<small id="allowedIPHelpBlock" class="form-text text-muted">
|
||||
Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32"
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idAdditionalInfo" class="col-sm-2 col-form-label">Additional info</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control" id="idAdditionalInfo" name="additional_info" rows="3"
|
||||
aria-describedby="additionalInfoHelpBlock">{{.Admin.AdditionalInfo}}</textarea>
|
||||
<small id="additionalInfoHelpBlock" class="form-text text-muted">
|
||||
Free form text field
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 mb-5 px-5 px-3">Submit</button>
|
||||
</form>
|
||||
|
||||
{{end}}
|
|
@ -84,6 +84,14 @@
|
|||
</li>
|
||||
{{end}}
|
||||
|
||||
{{ if .LoggedAdmin.HasPermission "manage_system"}}
|
||||
<li class="nav-item {{if eq .CurrentURL .MaintenanceURL}}active{{end}}">
|
||||
<a class="nav-link" href="{{.MaintenanceURL}}">
|
||||
<i class="fas fa-wrench"></i>
|
||||
<span>{{.MaintenanceTitle}}</span></a>
|
||||
</li>
|
||||
{{end}}
|
||||
|
||||
{{ if .LoggedAdmin.HasPermission "view_status"}}
|
||||
<li class="nav-item {{if eq .CurrentURL .StatusURL}}active{{end}}">
|
||||
<a class="nav-link" href="{{.StatusURL}}">
|
||||
|
|
|
@ -3,21 +3,27 @@
|
|||
{{define "title"}}{{.Title}}{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
<h1 class="h5 mb-4 text-gray-800">Add a new folder</h1>
|
||||
{{if .Error}}
|
||||
<div class="card mb-4 border-left-warning">
|
||||
<div class="card-body text-form-error">{{.Error}}</div>
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Add a new folder</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .Error}}
|
||||
<div class="card mb-4 border-left-warning">
|
||||
<div class="card-body text-form-error">{{.Error}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<form id="folder_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
|
||||
<div class="form-group row">
|
||||
<label for="idMappedPath" class="col-sm-2 col-form-label">Absolute Path</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idMappedPath" name="mapped_path" placeholder=""
|
||||
value="{{.Folder.MappedPath}}" maxlength="512" autocomplete="nope" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<form id="folder_form" action="{{.CurrentURL}}" method="POST" autocomplete="off">
|
||||
<div class="form-group row">
|
||||
<label for="idMappedPath" class="col-sm-2 col-form-label">Absolute Path</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idMappedPath" name="mapped_path" placeholder=""
|
||||
value="{{.Folder.MappedPath}}" maxlength="512" autocomplete="nope" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 mb-5 px-5 px-3">Submit</button>
|
||||
</form>
|
||||
{{end}}
|
64
templates/maintenance.html
Normal file
64
templates/maintenance.html
Normal file
|
@ -0,0 +1,64 @@
|
|||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{.Title}}{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Restore</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .Error}}
|
||||
<div class="card mb-4 border-left-warning">
|
||||
<div class="card-body text-form-error">{{.Error}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<form id="restore_form" enctype="multipart/form-data" action="{{.RestorePath}}" method="POST">
|
||||
<div class="form-group row">
|
||||
<label for="idBackupFile" class="col-sm-2 col-form-label">Backup file</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="file" class="form-control-file" id="idBackupFile" name="backup_file"
|
||||
aria-describedby="BackupFileHelpBlock">
|
||||
<small id="BackupFileHelpBlock" class="form-text text-muted">
|
||||
Restore data from a JSON backup file
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="idMode" class="col-sm-2 col-form-label">Mode</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idMode" name="mode">
|
||||
<option value="1">add only</option>
|
||||
<option value="0">add and update</option>
|
||||
<option value="2">add, update and disconnect</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="idQuota" class="col-sm-2 col-form-label">After restore</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idQuota" name="quota">
|
||||
<option value="0">no quota update</option>
|
||||
<option value="1">update quota</option>
|
||||
<option value="2">update quota if the user has quota restrictions</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5 px-3">Restore</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Backup</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<a class="btn btn-primary" href="{{.BackupPath}}?output-data=1" target="_blank">Backup your data</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "extra_js"}}
|
||||
|
||||
{{end}}
|
1079
templates/user.html
1079
templates/user.html
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue