From 80f5ccd3575c8f2540bf2f14ce8f435b87beab3e Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Fri, 22 Jan 2021 19:42:18 +0100 Subject: [PATCH] web admin: add backup/restore --- docs/defender.md | 2 +- docs/kms.md | 2 +- go.sum | 42 -- httpd/api_folder.go | 8 +- httpd/api_maintenance.go | 121 ++-- httpd/httpd.go | 3 + httpd/httpd_test.go | 197 ++++++- httpd/schema/openapi.yaml | 141 +++-- httpd/server.go | 4 + httpd/web.go | 74 +++ httpdtest/httpdtest.go | 55 +- pkgs/build.sh | 2 +- templates/admin.html | 173 +++--- templates/base.html | 8 + templates/folder.html | 36 +- templates/maintenance.html | 64 +++ templates/user.html | 1079 ++++++++++++++++++------------------ 17 files changed, 1231 insertions(+), 780 deletions(-) create mode 100644 templates/maintenance.html diff --git a/docs/defender.md b/docs/defender.md index eaae913f..a1cbd7ac 100644 --- a/docs/defender.md +++ b/docs/defender.md @@ -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. diff --git a/docs/kms.md b/docs/kms.md index 12e63c23..00706c3e 100644 --- a/docs/kms.md +++ b/docs/kms.md @@ -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: diff --git a/go.sum b/go.sum index afe034ee..8bf088d5 100644 --- a/go.sum +++ b/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= diff --git a/httpd/api_folder.go b/httpd/api_folder.go index 6e6f08b6..f8e2681c 100644 --- a/httpd/api_folder.go +++ b/httpd/api_folder.go @@ -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") diff --git a/httpd/api_maintenance.go b/httpd/api_maintenance.go index 650d75f6..64760cdb 100644 --- a/httpd/api_maintenance.go +++ b/httpd/api_maintenance.go @@ -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 diff --git a/httpd/httpd.go b/httpd/httpd.go index 3e0d3c51..a23cbe49 100644 --- a/httpd/httpd.go +++ b/httpd/httpd.go @@ -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" diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index dc3b0e51..13bc9391 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -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) diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml index 2e405d5d..07474787 100644 --- a/httpd/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -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: diff --git a/httpd/server.go b/httpd/server.go index 150dd2b2..c86e22d9 100644 --- a/httpd/server.go +++ b/httpd/server.go @@ -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) { diff --git a/httpd/web.go b/httpd/web.go index 48bb7526..779c193b 100644 --- a/httpd/web.go +++ b/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 { diff --git a/httpdtest/httpdtest.go b/httpdtest/httpdtest.go index 6dc16bce..1e8660c0 100644 --- a/httpdtest/httpdtest.go +++ b/httpdtest/httpdtest.go @@ -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) diff --git a/pkgs/build.sh b/pkgs/build.sh index c9efe78d..31ce7fa5 100755 --- a/pkgs/build.sh +++ b/pkgs/build.sh @@ -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 diff --git a/templates/admin.html b/templates/admin.html index 90fbe201..16b7b3e9 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -3,90 +3,93 @@ {{define "title"}}{{.Title}}{{end}} {{define "page_body"}} - -

{{if .IsAdd}}Add a new admin{{else}}Edit admin{{end}}

-{{if .Error}} -
-
{{.Error}}
+
+
+
{{if .IsAdd}}Add a new admin{{else}}Edit admin{{end}}
+
+
+ {{if .Error}} +
+
{{.Error}}
+
+ {{end}} +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ + {{if not .IsAdd}} + + If empty the current password will not be changed + + {{end}} +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ + + Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32" + +
+
+ +
+ +
+ + + Free form text field + +
+
+ + +
+
-{{end}} -
-
- -
- -
-
- -
- -
- -
-
- -
- -
- - {{if not .IsAdd}} - - If empty the current password will not be changed - - {{end}} -
-
- -
- -
- -
-
- -
- -
- -
-
- -
- -
- - - Comma separated IP/Mask in CIDR format, for example "192.168.1.0/24,10.8.0.100/32" - -
-
- -
- -
- - - Free form text field - -
-
- - -
- {{end}} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 9826a4f8..5a0f038a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -84,6 +84,14 @@ {{end}} + {{ if .LoggedAdmin.HasPermission "manage_system"}} + + {{end}} + {{ if .LoggedAdmin.HasPermission "view_status"}}
+ -
- -
- - - One exposed virtual directory per line as /dir::extension1,extension2, for example /subdir::.zip,.rar. Deprecated, use file patterns - -
-
+ + One exposed virtual directory per line as /dir::extension1,extension2, for example + /subdir::.zip,.rar. Deprecated, use file patterns + + + -
- -
- - - One exposed virtual directory per line as /dir::extension1,extension2, for example /somedir::.jpg,.png. Deprecated, use file patterns - -
-
+ + One exposed virtual directory per line as /dir::extension1,extension2, for example + /somedir::.jpg,.png. Deprecated, use file patterns + + + -
- -
- -
-
+
+ +
+ +
+
-
- -
- -
-
- -
- -
-
+
+ +
+ +
+
+ +
+ +
+
-
- -
- -
-
- -
- -
-
+
+ +
+ +
+
+ +
+ +
+
-
- -
- -
-
- -
- -
-
+
+ +
+ +
+
+ +
+ +
+
-
- -
- - - The buffer size for multipart uploads. Zero means the default (5 MB). Minimum is 5 - -
-
- -
- - - How many parts are uploaded in parallel. Zero means the default (2) - -
-
+
+ +
+ + + The buffer size for multipart uploads. Zero means the default (5 MB). Minimum is 5 + +
+
+ +
+ + + How many parts are uploaded in parallel. Zero means the default (2) + +
+
-
- -
- - - Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/". - -
-
+
+ +
+ + + Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/". + +
+
-
- -
- -
-
+
+ +
+ +
+
-
- -
- - - Add or update credentials from a JSON file - -
-
- -
- -
-
+
+ +
+ + + Add or update credentials from a JSON file + +
+
+ +
+ +
+
-
-
- - -
-
+
+
+ + +
+
-
- -
- - - Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/". - -
-
+
+ +
+ + + Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/". + +
+
-
- -
- -
-
- -
- -
-
+
+ +
+ +
+
+ +
+ +
+
-
- -
- -
-
+
+ +
+ +
+
-
- -
- -
-
-
- -
- -
-
+
+ +
+ +
+
+
+ +
+ +
+
-
- -
- -
-
+
+ +
+ +
+
-
- -
- - - The buffer size for multipart uploads. Zero means the default (4 MB) - -
-
- -
- - - How many parts are uploaded in parallel. Zero means the default (2) - -
-
+
+ +
+ + + The buffer size for multipart uploads. Zero means the default (4 MB) + +
+
+ +
+ + + How many parts are uploaded in parallel. Zero means the default (2) + +
+
-
- -
- - - Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/". - -
-
+
+ +
+ + + Similar to a chroot for local filesystem. Cannot start with "/". Example: "somedir/subdir/". + +
+
-
-
- - -
-
+
+
+ + +
+
-
- -
- -
-
+
+ +
+ +
+
-
- -
- -
-
- -
- -
-
+
+ +
+ +
+
+ +
+ +
+
-
- -
- -
-
+
+ +
+ +
+
-
- -
- -
-
+
+ +
+ +
+
-
- -
- - - SHA256 fingerprints to validate when connecting to the external SFTP server, one per line. If empty any host key will be accepted: this is a security risk! - -
-
+
+ +
+ + + SHA256 fingerprints to validate when connecting to the external SFTP server, one per line. If + empty any host key will be accepted: this is a security risk! + +
+
-
- -
- - - Similar to a chroot for local filesystem. Example: "/somedir/subdir". - -
-
+
+ +
+ + + Similar to a chroot for local filesystem. Example: "/somedir/subdir". + +
+
-
- -
- - - Free form text field - -
-
+
+ +
+ + + Free form text field + +
+
- {{if not .IsAdd}} -
-
- - - - This way you force the user to login again, if connected, and so to use the new configuration - -
-
- {{end}} + {{if not .IsAdd}} +
+
+ + + + This way you force the user to login again, if connected, and so to use the new configuration + +
+
+ {{end}} - - - + + + + + {{end}} {{define "extra_js"}}