diff --git a/go.mod b/go.mod
index 86a1d46e..35d45f24 100644
--- a/go.mod
+++ b/go.mod
@@ -9,35 +9,35 @@ require (
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
github.com/alexedwards/argon2id v1.0.0
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
- github.com/aws/aws-sdk-go-v2 v1.25.2
- github.com/aws/aws-sdk-go-v2/config v1.27.6
- github.com/aws/aws-sdk-go-v2/credentials v1.17.6
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2
- github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.8
- github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.1
- github.com/aws/aws-sdk-go-v2/service/s3 v1.51.3
- github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.1
- github.com/aws/aws-sdk-go-v2/service/sts v1.28.3
+ github.com/aws/aws-sdk-go-v2 v1.25.3
+ github.com/aws/aws-sdk-go-v2/config v1.27.7
+ github.com/aws/aws-sdk-go-v2/credentials v1.17.7
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.3
+ github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.9
+ github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.2
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.51.4
+ github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.2
+ github.com/aws/aws-sdk-go-v2/service/sts v1.28.4
github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/cockroachdb/cockroach-go/v2 v2.3.7
github.com/coreos/go-oidc/v3 v3.9.0
github.com/drakkan/webdav v0.0.0-20240212101318-94e905cb9adb
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
- github.com/fclairamb/ftpserverlib v0.22.0
- github.com/fclairamb/go-log v0.4.1
- github.com/go-acme/lego/v4 v4.15.0
+ github.com/fclairamb/ftpserverlib v0.24.0
+ github.com/fclairamb/go-log v0.5.0
+ github.com/go-acme/lego/v4 v4.16.1
github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/jwtauth/v5 v5.3.1
github.com/go-chi/render v1.0.3
- github.com/go-sql-driver/mysql v1.7.1
+ github.com/go-sql-driver/mysql v1.8.0
github.com/golang/mock v1.6.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0
github.com/hashicorp/go-hclog v1.6.2
github.com/hashicorp/go-plugin v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.5
- github.com/jackc/pgx/v5 v5.5.4
- github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
+ github.com/jackc/pgx/v5 v5.5.5
+ github.com/jlaffaye/ftp v0.2.0
github.com/klauspost/compress v1.17.7
github.com/lestrrat-go/jwx/v2 v2.0.21
github.com/lithammer/shortuuid/v3 v3.0.7
@@ -74,7 +74,7 @@ require (
golang.org/x/sys v0.18.0
golang.org/x/term v0.18.0
golang.org/x/time v0.5.0
- google.golang.org/api v0.168.0
+ google.golang.org/api v0.169.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
@@ -83,19 +83,20 @@ require (
cloud.google.com/go/compute v1.25.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.6 // indirect
+ filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 // indirect
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.3 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
- github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.2 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.4 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.4 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.2 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.5 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.3 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.20.2 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.2 // indirect
github.com/aws/smithy-go v1.20.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1 // indirect
@@ -108,7 +109,8 @@ require (
github.com/fatih/color v1.16.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
- github.com/go-jose/go-jose/v3 v3.0.2 // indirect
+ github.com/go-jose/go-jose/v3 v3.0.3 // indirect
+ github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
@@ -173,9 +175,9 @@ require (
golang.org/x/tools v0.19.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/appengine v1.6.8 // indirect
- google.golang.org/genproto v0.0.0-20240304212257-790db918fca8 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 // indirect
+ google.golang.org/genproto v0.0.0-20240311132316-a219d84964c2 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 // indirect
google.golang.org/grpc v1.62.1 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
diff --git a/go.sum b/go.sum
index 0221362d..402d4e64 100644
--- a/go.sum
+++ b/go.sum
@@ -11,6 +11,8 @@ cloud.google.com/go/kms v1.15.7 h1:7caV9K3yIxvlQPAcaFffhlT7d1qpxjB1wHBtjWa13SM=
cloud.google.com/go/kms v1.15.7/go.mod h1:ub54lbsa6tDkUwnu4W7Yt1aAIFLnspgh0kPGToDukeI=
cloud.google.com/go/storage v1.39.0 h1:brbjUa4hbDHhpQf48tjqMaXEV+f1OGoaTmQau9tmCsA=
cloud.google.com/go/storage v1.39.0/go.mod h1:OAEj/WZwUYjA3YHQ10/YcN9ttGuEpLwvaoyBXIPikEk=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0 h1:n1DH8TPV4qqPTje2RcUBYwtrTWlabVp4n46+74X2pn4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0/go.mod h1:HDcZnuGbiyppErN6lB+idp4CKhjbc8gwjto6OPpyggM=
@@ -33,46 +35,46 @@ github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHc
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/4wKeNvD/IQla3U3SUa61U7ul+xM4=
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k=
-github.com/aws/aws-sdk-go-v2 v1.25.2 h1:/uiG1avJRgLGiQM9X3qJM8+Qa6KRGK5rRPuXE0HUM+w=
-github.com/aws/aws-sdk-go-v2 v1.25.2/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo=
+github.com/aws/aws-sdk-go-v2 v1.25.3 h1:xYiLpZTQs1mzvz5PaI6uR0Wh57ippuEthxS4iK5v0n0=
+github.com/aws/aws-sdk-go-v2 v1.25.3/go.mod h1:35hUlJVYd+M++iLI3ALmVwMOyRYMmRqUXpTtRGW+K9I=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 h1:gTK2uhtAPtFcdRRJilZPx8uJLL2J85xK11nKtWL0wfU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1/go.mod h1:sxpLb+nZk7tIfCWChfd+h4QwHNUR57d8hA1cleTkjJo=
-github.com/aws/aws-sdk-go-v2/config v1.27.6 h1:WmoH1aPrxwcqAZTTnETjKr+fuvqzKd4hRrKxQUiuKP4=
-github.com/aws/aws-sdk-go-v2/config v1.27.6/go.mod h1:W9RZFF2pL+OhnUSZsQS/eDMWD8v+R+yWgjj3nSlrXVU=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.6 h1:akhj/nSC6SEx3OmiYGG/7mAyXMem9ZNVVf+DXkikcTk=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.6/go.mod h1:chJZuJ7TkW4kiMwmldOJOEueBoSkUb4ynZS1d9dhygo=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 h1:AK0J8iYBFeUk2Ax7O8YpLtFsfhdOByh2QIkHmigpRYk=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2/go.mod h1:iRlGzMix0SExQEviAyptRWRGdYNo3+ufW/lCzvKVTUc=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.8 h1:fjsaZ2EUoOaosuYMLbQAVJsPIAOV4Xn52AQmk5JbhAs=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.8/go.mod h1:WPJcs0Mze3WntafH9Df2NdJ1oSQkEQVL6piZxoS0ecY=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 h1:bNo4LagzUKbjdxE0tIcR9pMzLR2U/Tgie1Hq1HQ3iH8=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2/go.mod h1:wRQv0nN6v9wDXuWThpovGQjqF1HFdcgWjporw14lS8k=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 h1:EtOU5jsPdIQNP+6Q2C5e3d65NKT1PeCiQk+9OdzO12Q=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2/go.mod h1:tyF5sKccmDz0Bv4NrstEr+/9YkSPJHrcO7UsUKf7pWM=
+github.com/aws/aws-sdk-go-v2/config v1.27.7 h1:JSfb5nOQF01iOgxFI5OIKWwDiEXWTyTgg1Mm1mHi0A4=
+github.com/aws/aws-sdk-go-v2/config v1.27.7/go.mod h1:PH0/cNpoMO+B04qET699o5W92Ca79fVtbUnvMIZro4I=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.7 h1:WJd+ubWKoBeRh7A5iNMnxEOs982SyVKOJD+K8HIezu4=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.7/go.mod h1:UQi7LMR0Vhvs+44w5ec8Q+VS+cd10cjwgHwiVkE0YGU=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.3 h1:p+y7FvkK2dxS+FEwRIDHDe//ZX+jDhP8HHE50ppj4iI=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.3/go.mod h1:/fYB+FZbDlwlAiynK9KDXlzZl3ANI9JkD0Uhz5FjNT4=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.9 h1:vXY/Hq1XdxHBIYgBUmug/AbMyIe1AKulPYS2/VE1X70=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.9/go.mod h1:GyJJTZoHVuENM4TeJEl5Ffs4W9m19u+4wKJcDi/GZ4A=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.3 h1:ifbIbHZyGl1alsAhPIYsHOg5MuApgqOvVeI8wIugXfs=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.3/go.mod h1:oQZXg3c6SNeY6OZrDY+xHcF4VGIEoNotX2B4PrDeoJI=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.3 h1:Qvodo9gHG9F3E8SfYOspPeBt0bjSbsevK8WhRAUHcoY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.3/go.mod h1:vCKrdLXtybdf/uQd/YfVR2r5pcbNuEYKzMQpcxmeSJw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.2 h1:en92G0Z7xlksoOylkUhuBSfJgijC7rHVLRdnIlHEs0E=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.2/go.mod h1:HgtQ/wN5G+8QSlK62lbOtNwQ3wTSByJ4wH2rCkPt+AE=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.3 h1:mDnFOE2sVkyphMWtTH+stv0eW3k0OTx94K63xpxHty4=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.3/go.mod h1:V8MuRVcCRt5h1S+Fwu8KbC7l/gBGo3yBAyUbJM2IJOk=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.4 h1:J3Q6N2sTChfYLZSTey3Qeo7n3JSm6RTJDcKev+7Sbus=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.4/go.mod h1:ZopsdDMVg1H03X7BdzpGaufOkuz27RjtKDzioP2U0Hg=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.4 h1:jRiWxyuVO8PlkN72wDMVn/haVH4SDCBkUt0Lf/dxd7s=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.4/go.mod h1:Ru7vg1iQ7cR4i7SZ/JTLYN9kaXtbL69UdgG0OQWQxW0=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.2 h1:1oY1AVEisRI4HNuFoLdRUB0hC63ylDAN6Me3MrfclEg=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.2/go.mod h1:KZ03VgvZwSjkT7fOetQ/wF3MZUvYFirlI1H5NklUNsY=
-github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.1 h1:cy+x/R8zd4Zluf+6ZWzbPPdLh+l4MeYPlYxdqK+Qr0M=
-github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.1/go.mod h1:ITcPsa7HwiY6ddwbwmtuf+/q7sfr4MjH5HzUvn5FHBQ=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.51.3 h1:7cR4xxS480TI0R6Bd75g9Npdw89VriquvQPlMNmuds4=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.51.3/go.mod h1:zb72GZ2MvfCX5ynVJ+Mc/NCx7hncbsko4NZm5E+p6J4=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.1 h1:DtKw4TxZT3VrzYupXQJPBqT9ImyobZZE+JIQPPAVxqs=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.1/go.mod h1:bit9G2ORpSjUTr4PA4usvbBfbOyvMj0LbE1dXF14Sug=
-github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 h1:utEGkfdQ4L6YW/ietH7111ZYglLJvS+sLriHJ1NBJEQ=
-github.com/aws/aws-sdk-go-v2/service/sso v1.20.1/go.mod h1:RsYqzYr2F2oPDdpy+PdhephuZxTfjHQe7SOBcZGoAU8=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 h1:9/GylMS45hGGFCcMrUZDVayQE1jYSIN6da9jo7RAYIw=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1/go.mod h1:YjAPFn4kGFqKC54VsHs5fn5B6d+PCY2tziEa3U/GB5Y=
-github.com/aws/aws-sdk-go-v2/service/sts v1.28.3 h1:TkiFkSVX990ryWIMBCT4kPqZEgThQe1xPU/AQXavtvU=
-github.com/aws/aws-sdk-go-v2/service/sts v1.28.3/go.mod h1:xYNauIUqSuvzlPVb3VB5no/n48YGhmlInD3Uh0Co8Zc=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.5 h1:mbWNpfRUTT6bnacmvOTKXZjR/HycibdWzNpfbrbLDIs=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.5/go.mod h1:FCOPWGjsshkkICJIn9hq9xr6dLKtyaWpuUojiN3W1/8=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.5 h1:K/NXvIftOlX+oGgWGIa3jDyYLDNsdVhsjHmsBH2GLAQ=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.5/go.mod h1:cl9HGLV66EnCmMNzq4sYOti+/xo8w34CsgzVtm2GgsY=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.3 h1:4t+QEX7BsXz98W8W1lNvMAG+NX8qHz2CjLBxQKku40g=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.3/go.mod h1:oFcjjUq5Hm09N9rpxTdeMeLeQcxS7mIkBkL8qUKng+A=
+github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.2 h1:SxqH8OxVZ804yweHp2bNERqys7om9cmjxowlI/XOnS8=
+github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.21.2/go.mod h1:Djusz31OLPJV5vma0ujfKxgsl8gBezwLZh5LA5044fk=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.51.4 h1:lW5xUzOPGAMY7HPuNF4FdyBwRc3UJ/e8KsapbesVeNU=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.51.4/go.mod h1:MGTaf3x/+z7ZGugCGvepnx2DS6+caCYYqKhzVoLNYPk=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.2 h1:WrqqLhD5St2cbXsvR0yuY43pdhXsUL0yjQepBJIpTvI=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.2/go.mod h1:GvNHKQAAOSKjmlccE/+Ww2gDbwYP9EewIuvWiQSquQs=
+github.com/aws/aws-sdk-go-v2/service/sso v1.20.2 h1:XOPfar83RIRPEzfihnp+U6udOveKZJvPQ76SKWrLRHc=
+github.com/aws/aws-sdk-go-v2/service/sso v1.20.2/go.mod h1:Vv9Xyk1KMHXrR3vNQe8W5LMFdTjSeWk0gBZBzvf3Qa0=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.2 h1:pi0Skl6mNl2w8qWZXcdOyg197Zsf4G97U7Sso9JXGZE=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.2/go.mod h1:JYzLoEVeLXk+L4tn1+rrkfhkxl6mLDEVaDSvGq9og90=
+github.com/aws/aws-sdk-go-v2/service/sts v1.28.4 h1:Ppup1nVNAOWbBOrcoOxaxPeEnSFB2RnnQdguhXpmeQk=
+github.com/aws/aws-sdk-go-v2/service/sts v1.28.4/go.mod h1:+K1rNPVyGxkRuv9NNiaZ4YhBFuyw2MMA9SlIJ1Zlpz8=
github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw=
github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -128,24 +130,26 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
-github.com/fclairamb/go-log v0.4.1 h1:rLtdSG9x2pK41AIAnE8WYpl05xBJfw1ZyYxZaXFcBsM=
-github.com/fclairamb/go-log v0.4.1/go.mod h1:sw1KvnkZ4wKCYkvy4SL3qVZcJSWFP8Ure4pM3z+KNn4=
+github.com/fclairamb/go-log v0.5.0 h1:Gz9wSamEaA6lta4IU2cjJc2xSq5sV5VYSB5w/SUHhVc=
+github.com/fclairamb/go-log v0.5.0/go.mod h1:XoRO1dYezpsGmLLkZE9I+sHqpqY65p8JA+Vqblb7k40=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/go-acme/lego/v4 v4.15.0 h1:A7MHEU3b+TDFqhC/HmzMJnzPbyeaYvMZQBbqgvbThhU=
-github.com/go-acme/lego/v4 v4.15.0/go.mod h1:eeGhjW4zWT7Ccqa3sY7ayEqFLCAICx+mXgkMHKIkLxg=
+github.com/go-acme/lego/v4 v4.16.1 h1:JxZ93s4KG0jL27rZ30UsIgxap6VGzKuREsSkkyzeoCQ=
+github.com/go-acme/lego/v4 v4.16.1/go.mod h1:AVvwdPned/IWpD/ihHhMsKnveF7HHYAz/CmtXi7OZoE=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A=
github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
-github.com/go-jose/go-jose/v3 v3.0.2 h1:2Edjn8Nrb44UvTdp84KU0bBPs1cO7noRCybtS3eJEUQ=
-github.com/go-jose/go-jose/v3 v3.0.2/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
+github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
+github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
+github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
+github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
@@ -158,8 +162,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
-github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
-github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
+github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -238,8 +242,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8=
-github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
+github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
+github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
@@ -522,8 +526,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
-google.golang.org/api v0.168.0 h1:MBRe+Ki4mMN93jhDDbpuRLjRddooArz4FeSObvUMmjY=
-google.golang.org/api v0.168.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg=
+google.golang.org/api v0.169.0 h1:QwWPy71FgMWqJN/l6jVlFHUa29a7dcUy02I8o799nPY=
+google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
@@ -531,12 +535,12 @@ google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJ
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20240304212257-790db918fca8 h1:Fe8QycXyEd9mJgnwB9kmw00WgB43eQ/xYO5C6gceybQ=
-google.golang.org/genproto v0.0.0-20240304212257-790db918fca8/go.mod h1:yA7a1bW1kwl459Ol0m0lV4hLTfrL/7Bkk4Mj2Ir1mWI=
-google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8 h1:8eadJkXbwDEMNwcB5O0s5Y5eCfyuCLdvaiOIaGTrWmQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 h1:IR+hp6ypxjH24bkMfEJ0yHR21+gwPWdV+/IBrPQyn3k=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
+google.golang.org/genproto v0.0.0-20240311132316-a219d84964c2 h1:rrOOzm+NteCjTNqCnDAdYhvKL1G/9N/Lj1GRxJtQEL0=
+google.golang.org/genproto v0.0.0-20240311132316-a219d84964c2/go.mod h1:yA7a1bW1kwl459Ol0m0lV4hLTfrL/7Bkk4Mj2Ir1mWI=
+google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ=
+google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 h1:9IZDv+/GcI6u+a4jRFRLxQs0RUCfavGfoOgEW6jpkI0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
diff --git a/internal/dataprovider/session.go b/internal/dataprovider/session.go
index 5065ec8c..a40c42d8 100644
--- a/internal/dataprovider/session.go
+++ b/internal/dataprovider/session.go
@@ -29,6 +29,7 @@ const (
SessionTypeResetCode
SessionTypeOAuth2Auth
SessionTypeInvalidToken
+ SessionTypeWebTask
)
// Session defines a shared session persisted in the data provider
@@ -43,7 +44,7 @@ func (s *Session) validate() error {
if s.Key == "" {
return errors.New("unable to save a session with an empty key")
}
- if s.Type < SessionTypeOIDCAuth || s.Type > SessionTypeInvalidToken {
+ if s.Type < SessionTypeOIDCAuth || s.Type > SessionTypeWebTask {
return fmt.Errorf("invalid session type: %v", s.Type)
}
return nil
diff --git a/internal/dataprovider/sqlcommon.go b/internal/dataprovider/sqlcommon.go
index f3977f45..8daf1efc 100644
--- a/internal/dataprovider/sqlcommon.go
+++ b/internal/dataprovider/sqlcommon.go
@@ -3247,6 +3247,9 @@ func sqlCommonGetSession(key string, dbHandle sqlQuerier) (Session, error) {
var data []byte // type hint, some driver will use string instead of []byte if the type is any
err := dbHandle.QueryRowContext(ctx, q, key).Scan(&session.Key, &data, &session.Type, &session.Timestamp)
if err != nil {
+ if errors.Is(err, sql.ErrNoRows) {
+ return session, util.NewRecordNotFoundError(err.Error())
+ }
return session, err
}
session.Data = data
diff --git a/internal/httpd/httpd.go b/internal/httpd/httpd.go
index c9f3e0ac..13a69428 100644
--- a/internal/httpd/httpd.go
+++ b/internal/httpd/httpd.go
@@ -177,6 +177,7 @@ const (
webClientViewPDFPathDefault = "/web/client/viewpdf"
webClientGetPDFPathDefault = "/web/client/getpdf"
webClientExistPathDefault = "/web/client/exist"
+ webClientTasksPathDefault = "/web/client/tasks"
webStaticFilesPathDefault = "/static"
webOpenAPIPathDefault = "/openapi"
// MaxRestoreSize defines the max size for the loaddata input file
@@ -278,6 +279,7 @@ var (
webClientViewPDFPath string
webClientGetPDFPath string
webClientExistPath string
+ webClientTasksPath string
webStaticFilesPath string
webOpenAPIPath string
// max upload size for http clients, 1GB by default
@@ -936,6 +938,7 @@ func (c *Conf) Initialize(configDir string, isShared int) error {
resetCodesMgr = newResetCodeManager(isShared)
oidcMgr = newOIDCManager(isShared)
oauth2Mgr = newOAuth2Manager(isShared)
+ webTaskMgr = newWebTaskManager(isShared)
staticFilesPath := util.FindSharedDataPath(c.StaticFilesPath, configDir)
templatesPath := util.FindSharedDataPath(c.TemplatesPath, configDir)
openAPIPath := util.FindSharedDataPath(c.OpenAPIPath, configDir)
@@ -1108,6 +1111,7 @@ func updateWebClientURLs(baseURL string) {
webClientViewPDFPath = path.Join(baseURL, webClientViewPDFPathDefault)
webClientGetPDFPath = path.Join(baseURL, webClientGetPDFPathDefault)
webClientExistPath = path.Join(baseURL, webClientExistPathDefault)
+ webClientTasksPath = path.Join(baseURL, webClientTasksPathDefault)
webStaticFilesPath = path.Join(baseURL, webStaticFilesPathDefault)
webOpenAPIPath = path.Join(baseURL, webOpenAPIPathDefault)
}
@@ -1196,6 +1200,7 @@ func startCleanupTicker(duration time.Duration) {
counter++
invalidatedJWTTokens.Cleanup()
resetCodesMgr.Cleanup()
+ webTaskMgr.Cleanup()
if counter%2 == 0 {
oidcMgr.cleanup()
oauth2Mgr.cleanup()
diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go
index cbfa4966..d803c58f 100644
--- a/internal/httpd/httpd_test.go
+++ b/internal/httpd/httpd_test.go
@@ -193,6 +193,9 @@ const (
webClientViewPDFPath = "/web/client/viewpdf"
webClientGetPDFPath = "/web/client/getpdf"
webClientExistPath = "/web/client/exist"
+ webClientTasksPath = "/web/client/tasks"
+ webClientFileMovePath = "/web/client/file-actions/move"
+ webClientFileCopyPath = "/web/client/file-actions/copy"
jsonAPISuffix = "/json"
httpBaseURL = "http://127.0.0.1:8081"
defaultRemoteAddr = "127.0.0.1:1234"
@@ -16505,6 +16508,28 @@ func TestRenameDifferentResource(t *testing.T) {
testFileName := "file.txt"
webAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
+ assert.NoError(t, err)
+ webToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
+ assert.NoError(t, err)
+ csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath)
+
+ getStatusResponse := func(taskID string) int {
+ req, _ := http.NewRequest(http.MethodGet, webClientTasksPath+"/"+url.PathEscape(taskID), nil)
+ req.RemoteAddr = defaultRemoteAddr
+ req.Header.Set("X-CSRF-TOKEN", csrfToken)
+ setJWTCookieForReq(req, webToken)
+ rr := executeRequest(req)
+ if rr.Code != http.StatusOK {
+ return -1
+ }
+ resp := make(map[string]any)
+ err = json.Unmarshal(rr.Body.Bytes(), &resp)
+ if err != nil {
+ return -1
+ }
+ return int(resp["status"].(float64))
+ }
+
assert.NoError(t, err)
req, err := http.NewRequest(http.MethodPost, userFileActionsPath+"/move?path="+testFileName+"&target="+url.QueryEscape(path.Join("/", "folderPath", testFileName)), nil) //nolint:goconst
@@ -16514,6 +16539,24 @@ func TestRenameDifferentResource(t *testing.T) {
checkResponseCode(t, http.StatusNotFound, rr)
assert.Contains(t, rr.Body.String(), "Cannot perform copy step")
+ req, err = http.NewRequest(http.MethodPost, webClientFileMovePath+"?path="+testFileName+"&target="+url.QueryEscape(path.Join("/", "folderPath", testFileName)), nil)
+ assert.NoError(t, err)
+ req.RemoteAddr = defaultRemoteAddr
+ req.Header.Set("X-CSRF-TOKEN", csrfToken)
+ setJWTCookieForReq(req, webToken)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusAccepted, rr)
+ taskResp := make(map[string]any)
+ err = json.Unmarshal(rr.Body.Bytes(), &taskResp)
+ assert.NoError(t, err)
+ taskID := taskResp["message"].(string)
+ assert.NotEmpty(t, taskID)
+
+ assert.Eventually(t, func() bool {
+ status := getStatusResponse(taskID)
+ return status == http.StatusNotFound
+ }, 1000*time.Millisecond, 100*time.Millisecond)
+
err = os.WriteFile(filepath.Join(user.GetHomeDir(), testFileName), []byte("just a test"), os.ModePerm)
assert.NoError(t, err)
@@ -16557,6 +16600,24 @@ func TestRenameDifferentResource(t *testing.T) {
checkResponseCode(t, http.StatusForbidden, rr)
assert.Contains(t, rr.Body.String(), "Cannot perform remove step")
+ req, err = http.NewRequest(http.MethodPost, webClientFileMovePath+"?path="+testFileName+"&target="+url.QueryEscape(path.Join("/", "folderPath", testFileName)), nil)
+ assert.NoError(t, err)
+ req.RemoteAddr = defaultRemoteAddr
+ req.Header.Set("X-CSRF-TOKEN", csrfToken)
+ setJWTCookieForReq(req, webToken)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusAccepted, rr)
+ taskResp = make(map[string]any)
+ err = json.Unmarshal(rr.Body.Bytes(), &taskResp)
+ assert.NoError(t, err)
+ taskID = taskResp["message"].(string)
+ assert.NotEmpty(t, taskID)
+
+ assert.Eventually(t, func() bool {
+ status := getStatusResponse(taskID)
+ return status == http.StatusForbidden
+ }, 1000*time.Millisecond, 100*time.Millisecond)
+
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
@@ -17185,6 +17246,209 @@ func TestBufferedWebFilesAPI(t *testing.T) {
assert.NoError(t, err)
}
+func TestWebClientTasksAPI(t *testing.T) {
+ user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
+ assert.NoError(t, err)
+ u1 := getTestUser()
+ u1.Username = xid.New().String()
+ user1, _, err := httpdtest.AddUser(u1, http.StatusCreated)
+ assert.NoError(t, err)
+
+ testDir := "subdir"
+ testFileData := []byte("data")
+ testFilePath := filepath.Join(user.GetHomeDir(), testDir, "file.txt")
+ testFileName := filepath.Base(testFilePath)
+ err = os.MkdirAll(filepath.Dir(testFilePath), os.ModePerm)
+ assert.NoError(t, err)
+ err = os.WriteFile(testFilePath, testFileData, 0666)
+ assert.NoError(t, err)
+
+ webToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
+ assert.NoError(t, err)
+ csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath)
+ assert.NoError(t, err)
+ webToken1, err := getJWTWebClientTokenFromTestServer(user1.Username, defaultPassword)
+ assert.NoError(t, err)
+
+ getStatusResponse := func(taskID string) int {
+ req, _ := http.NewRequest(http.MethodGet, webClientTasksPath+"/"+url.PathEscape(taskID), nil)
+ req.RemoteAddr = defaultRemoteAddr
+ req.Header.Set("X-CSRF-TOKEN", csrfToken)
+ setJWTCookieForReq(req, webToken)
+ rr := executeRequest(req)
+ if rr.Code != http.StatusOK {
+ return -1
+ }
+ resp := make(map[string]any)
+ err = json.Unmarshal(rr.Body.Bytes(), &resp)
+ if err != nil {
+ return -1
+ }
+ return int(resp["status"].(float64))
+ }
+ // missing task
+ assert.Equal(t, -1, getStatusResponse("missing"))
+
+ req, err := http.NewRequest(http.MethodPost, webClientFileCopyPath+"?path="+
+ url.QueryEscape(path.Join(testDir, testFileName))+"&target="+url.QueryEscape(testFileName), nil)
+ assert.NoError(t, err)
+ req.RemoteAddr = defaultRemoteAddr
+ req.Header.Set("X-CSRF-TOKEN", csrfToken)
+ setJWTCookieForReq(req, webToken)
+ rr := executeRequest(req)
+ checkResponseCode(t, http.StatusAccepted, rr)
+ resp := make(map[string]any)
+ err = json.Unmarshal(rr.Body.Bytes(), &resp)
+ assert.NoError(t, err)
+ taskID := resp["message"].(string)
+ assert.NotEmpty(t, taskID)
+
+ assert.Eventually(t, func() bool {
+ status := getStatusResponse(taskID)
+ return status == http.StatusOK
+ }, 1000*time.Millisecond, 100*time.Millisecond)
+
+ // cannot get the task with a different user
+ req, err = http.NewRequest(http.MethodGet, webClientTasksPath+"/"+url.PathEscape(taskID), nil)
+ assert.NoError(t, err)
+ req.RemoteAddr = defaultRemoteAddr
+ req.Header.Set("X-CSRF-TOKEN", csrfToken)
+ setJWTCookieForReq(req, webToken1)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusForbidden, rr)
+
+ req, err = http.NewRequest(http.MethodPost, webClientFileMovePath+"?path="+
+ url.QueryEscape(path.Join(testDir, testFileName))+"&target="+url.QueryEscape(testFileName), nil)
+ assert.NoError(t, err)
+ req.RemoteAddr = defaultRemoteAddr
+ req.Header.Set("X-CSRF-TOKEN", csrfToken)
+ setJWTCookieForReq(req, webToken)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusAccepted, rr)
+ resp = make(map[string]any)
+ err = json.Unmarshal(rr.Body.Bytes(), &resp)
+ assert.NoError(t, err)
+ taskID = resp["message"].(string)
+ assert.NotEmpty(t, taskID)
+
+ assert.Eventually(t, func() bool {
+ status := getStatusResponse(taskID)
+ return status == http.StatusOK
+ }, 1000*time.Millisecond, 100*time.Millisecond)
+
+ req, err = http.NewRequest(http.MethodDelete, webClientDirsPath+"?path="+
+ url.QueryEscape(testDir), nil)
+ assert.NoError(t, err)
+ req.RemoteAddr = defaultRemoteAddr
+ req.Header.Set("X-CSRF-TOKEN", csrfToken)
+ setJWTCookieForReq(req, webToken)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusAccepted, rr)
+ resp = make(map[string]any)
+ err = json.Unmarshal(rr.Body.Bytes(), &resp)
+ assert.NoError(t, err)
+ taskID = resp["message"].(string)
+ assert.NotEmpty(t, taskID)
+
+ assert.Eventually(t, func() bool {
+ status := getStatusResponse(taskID)
+ return status == http.StatusOK
+ }, 1000*time.Millisecond, 100*time.Millisecond)
+
+ req, err = http.NewRequest(http.MethodDelete, webClientDirsPath+"?path="+
+ url.QueryEscape(testDir), nil)
+ assert.NoError(t, err)
+ req.RemoteAddr = defaultRemoteAddr
+ req.Header.Set("X-CSRF-TOKEN", csrfToken)
+ setJWTCookieForReq(req, webToken)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusAccepted, rr)
+ resp = make(map[string]any)
+ err = json.Unmarshal(rr.Body.Bytes(), &resp)
+ assert.NoError(t, err)
+ taskID = resp["message"].(string)
+ assert.NotEmpty(t, taskID)
+
+ assert.Eventually(t, func() bool {
+ status := getStatusResponse(taskID)
+ return status == http.StatusNotFound
+ }, 1000*time.Millisecond, 100*time.Millisecond)
+
+ req, err = http.NewRequest(http.MethodPost, webClientFileMovePath+"?path="+
+ url.QueryEscape(path.Join(testDir, testFileName))+"&target="+url.QueryEscape(testFileName), nil)
+ assert.NoError(t, err)
+ req.RemoteAddr = defaultRemoteAddr
+ req.Header.Set("X-CSRF-TOKEN", csrfToken)
+ setJWTCookieForReq(req, webToken)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusAccepted, rr)
+ resp = make(map[string]any)
+ err = json.Unmarshal(rr.Body.Bytes(), &resp)
+ assert.NoError(t, err)
+ taskID = resp["message"].(string)
+ assert.NotEmpty(t, taskID)
+
+ assert.Eventually(t, func() bool {
+ status := getStatusResponse(taskID)
+ return status == http.StatusNotFound
+ }, 1000*time.Millisecond, 100*time.Millisecond)
+
+ req, err = http.NewRequest(http.MethodPost, webClientFileCopyPath+"?path="+
+ url.QueryEscape(path.Join(testDir, testFileName)+"/")+"&target="+url.QueryEscape(testFileName+"/"), nil)
+ assert.NoError(t, err)
+ req.RemoteAddr = defaultRemoteAddr
+ req.Header.Set("X-CSRF-TOKEN", csrfToken)
+ setJWTCookieForReq(req, webToken)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusAccepted, rr)
+ resp = make(map[string]any)
+ err = json.Unmarshal(rr.Body.Bytes(), &resp)
+ assert.NoError(t, err)
+ taskID = resp["message"].(string)
+ assert.NotEmpty(t, taskID)
+
+ assert.Eventually(t, func() bool {
+ status := getStatusResponse(taskID)
+ return status == http.StatusNotFound
+ }, 1000*time.Millisecond, 100*time.Millisecond)
+
+ _, err = httpdtest.RemoveUser(user, http.StatusOK)
+ assert.NoError(t, err)
+ err = os.RemoveAll(user.GetHomeDir())
+ assert.NoError(t, err)
+ _, err = httpdtest.RemoveUser(user1, http.StatusOK)
+ assert.NoError(t, err)
+ err = os.RemoveAll(user1.GetHomeDir())
+ assert.NoError(t, err)
+ // user deleted
+ req, err = http.NewRequest(http.MethodDelete, webClientDirsPath+"?path="+
+ url.QueryEscape(testDir), nil)
+ assert.NoError(t, err)
+ req.RemoteAddr = defaultRemoteAddr
+ req.Header.Set("X-CSRF-TOKEN", csrfToken)
+ setJWTCookieForReq(req, webToken)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusNotFound, rr)
+
+ req, err = http.NewRequest(http.MethodPost, webClientFileMovePath+"?path="+
+ url.QueryEscape(path.Join(testDir, testFileName))+"&target="+url.QueryEscape(testFileName), nil)
+ assert.NoError(t, err)
+ req.RemoteAddr = defaultRemoteAddr
+ req.Header.Set("X-CSRF-TOKEN", csrfToken)
+ setJWTCookieForReq(req, webToken)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusNotFound, rr)
+
+ req, err = http.NewRequest(http.MethodPost, webClientFileCopyPath+"?path="+
+ url.QueryEscape(path.Join(testDir, testFileName))+"&target="+url.QueryEscape(testFileName), nil)
+ assert.NoError(t, err)
+ req.RemoteAddr = defaultRemoteAddr
+ req.Header.Set("X-CSRF-TOKEN", csrfToken)
+ setJWTCookieForReq(req, webToken)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusNotFound, rr)
+}
+
func TestStartDirectory(t *testing.T) {
u := getTestUser()
u.Filters.StartDirectory = "/start/dir"
diff --git a/internal/httpd/internal_test.go b/internal/httpd/internal_test.go
index 1dfb8975..b47b6c91 100644
--- a/internal/httpd/internal_test.go
+++ b/internal/httpd/internal_test.go
@@ -19,7 +19,6 @@ import (
"context"
"crypto/tls"
"crypto/x509"
- "database/sql"
"encoding/json"
"errors"
"fmt"
@@ -286,6 +285,7 @@ RKjnkiEZeG4+G91Xu7+HmcBLwV86k5I+tXK9O1Okomr6Zry8oqVcxU5TB6VRS+rA
ubwF00Drdvk2+kDZfxIM137nBiy7wgCJi2Ksm5ihN3dUF6Q0oNPl
-----END RSA PRIVATE KEY-----`
defaultAdminUsername = "admin"
+ defeaultUsername = "test_user"
)
var (
@@ -521,6 +521,11 @@ func TestInvalidToken(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
+ rr = httptest.NewRecorder()
+ getWebTask(rr, req)
+ assert.Equal(t, http.StatusBadRequest, rr.Code)
+ assert.Contains(t, rr.Body.String(), "Invalid token claims")
+
rr = httptest.NewRecorder()
getAdminProfile(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
@@ -3167,7 +3172,7 @@ func TestDbResetCodeManager(t *testing.T) {
assert.ErrorIs(t, err, util.ErrNotFound)
}
_, err = mgr.Get(resetCode.Code)
- assert.ErrorIs(t, err, sql.ErrNoRows)
+ assert.ErrorIs(t, err, util.ErrNotFound)
// add an expired reset code
resetCode = newResetCode("user", false)
resetCode.ExpiresAt = time.Now().Add(-24 * time.Hour)
@@ -3179,7 +3184,7 @@ func TestDbResetCodeManager(t *testing.T) {
}
mgr.Cleanup()
_, err = mgr.Get(resetCode.Code)
- assert.ErrorIs(t, err, sql.ErrNoRows)
+ assert.ErrorIs(t, err, util.ErrNotFound)
dbMgr, ok := mgr.(*dbResetCodeManager)
if assert.True(t, ok) {
diff --git a/internal/httpd/server.go b/internal/httpd/server.go
index 52a1d7be..80cf2747 100644
--- a/internal/httpd/server.go
+++ b/internal/httpd/server.go
@@ -1583,6 +1583,8 @@ func (s *httpdServer) setupWebClientRoutes() {
router.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientViewPDFPath, s.handleClientViewPDF)
router.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientGetPDFPath, s.handleClientGetPDF)
router.With(s.checkAuthRequirements, s.refreshCookie, verifyCSRFHeader).Get(webClientFilePath, getUserFile)
+ router.With(s.checkAuthRequirements, s.refreshCookie, verifyCSRFHeader).Get(webClientTasksPath+"/{id}",
+ getWebTask)
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
Post(webClientFilePath, uploadUserFile)
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
@@ -1595,11 +1597,11 @@ func (s *httpdServer) setupWebClientRoutes() {
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
Post(webClientDirsPath, createUserDir)
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
- Delete(webClientDirsPath, deleteUserDir)
+ Delete(webClientDirsPath, taskDeleteDir)
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
- Post(webClientFileActionsPath+"/move", renameUserFsEntry)
+ Post(webClientFileActionsPath+"/move", taskRenameFsEntry)
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
- Post(webClientFileActionsPath+"/copy", copyUserFsEntry)
+ Post(webClientFileActionsPath+"/copy", taskCopyFsEntry)
router.With(s.checkAuthRequirements, s.refreshCookie).
Post(webClientDownloadZipPath, s.handleWebClientDownloadZip)
router.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientPingPath, handlePingRequest)
diff --git a/internal/httpd/webclient.go b/internal/httpd/webclient.go
index c407e2b2..4699ecf1 100644
--- a/internal/httpd/webclient.go
+++ b/internal/httpd/webclient.go
@@ -129,6 +129,7 @@ type filesPage struct {
DownloadURL string
ViewPDFURL string
FileURL string
+ TasksURL string
CanAddFiles bool
CanCreateDirs bool
CanRename bool
@@ -750,6 +751,7 @@ func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Reque
FileURL: "",
FileActionsURL: "",
CheckExistURL: path.Join(baseSharePath, "browse", "exist"),
+ TasksURL: "",
CanAddFiles: share.Scope == dataprovider.ShareScopeReadWrite,
CanCreateDirs: false,
CanRename: false,
@@ -793,6 +795,7 @@ func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, di
FileURL: webClientFilePath,
FileActionsURL: webClientFileActionsPath,
CheckExistURL: webClientExistPath,
+ TasksURL: webClientTasksPath,
CanAddFiles: user.CanAddFilesFromWeb(dirName),
CanCreateDirs: user.CanAddDirsFromWeb(dirName),
CanRename: user.CanRenameFromWeb(dirName, dirName),
@@ -2015,3 +2018,204 @@ func checkShareRedirectURL(next, base string) (bool, string) {
}
return true, next
}
+
+func getWebTask(w http.ResponseWriter, r *http.Request) {
+ r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
+ claims, err := getTokenClaims(r)
+ if err != nil || claims.Username == "" {
+ sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
+ return
+ }
+ taskID := getURLParam(r, "id")
+
+ task, err := webTaskMgr.Get(taskID)
+ if err != nil {
+ sendAPIResponse(w, r, err, "Unable to get task", getMappedStatusCode(err))
+ return
+ }
+ if task.User != claims.Username {
+ sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+ return
+ }
+ render.JSON(w, r, task)
+}
+
+func taskDeleteDir(w http.ResponseWriter, r *http.Request) {
+ r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+ connection, err := getUserConnection(w, r)
+ if err != nil {
+ return
+ }
+
+ name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
+ task := webTaskData{
+ ID: connection.GetID(),
+ User: connection.GetUsername(),
+ Path: name,
+ Timestamp: util.GetTimeAsMsSinceEpoch(time.Now()),
+ Status: 0,
+ }
+ if err := webTaskMgr.Add(task); err != nil {
+ common.Connections.Remove(connection.GetID())
+ sendAPIResponse(w, r, nil, "Unable to create task", http.StatusInternalServerError)
+ return
+ }
+ go executeDeleteTask(connection, task)
+ sendAPIResponse(w, r, nil, task.ID, http.StatusAccepted)
+}
+
+func taskRenameFsEntry(w http.ResponseWriter, r *http.Request) {
+ r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+ connection, err := getUserConnection(w, r)
+ if err != nil {
+ return
+ }
+ oldName := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
+ newName := connection.User.GetCleanedPath(r.URL.Query().Get("target"))
+ task := webTaskData{
+ ID: connection.GetID(),
+ User: connection.GetUsername(),
+ Path: oldName,
+ Target: newName,
+ Timestamp: util.GetTimeAsMsSinceEpoch(time.Now()),
+ Status: 0,
+ }
+ if err := webTaskMgr.Add(task); err != nil {
+ common.Connections.Remove(connection.GetID())
+ sendAPIResponse(w, r, nil, "Unable to create task", http.StatusInternalServerError)
+ return
+ }
+ go executeRenameTask(connection, task)
+ sendAPIResponse(w, r, nil, task.ID, http.StatusAccepted)
+}
+
+func taskCopyFsEntry(w http.ResponseWriter, r *http.Request) {
+ r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+ connection, err := getUserConnection(w, r)
+ if err != nil {
+ return
+ }
+ source := r.URL.Query().Get("path")
+ target := r.URL.Query().Get("target")
+ copyFromSource := strings.HasSuffix(source, "/")
+ copyInTarget := strings.HasSuffix(target, "/")
+ source = connection.User.GetCleanedPath(source)
+ target = connection.User.GetCleanedPath(target)
+ if copyFromSource {
+ source += "/"
+ }
+ if copyInTarget {
+ target += "/"
+ }
+ task := webTaskData{
+ ID: connection.GetID(),
+ User: connection.GetUsername(),
+ Path: source,
+ Target: target,
+ Timestamp: util.GetTimeAsMsSinceEpoch(time.Now()),
+ Status: 0,
+ }
+ if err := webTaskMgr.Add(task); err != nil {
+ common.Connections.Remove(connection.GetID())
+ sendAPIResponse(w, r, nil, "Unable to create task", http.StatusInternalServerError)
+ return
+ }
+ go executeCopyTask(connection, task)
+ sendAPIResponse(w, r, nil, task.ID, http.StatusAccepted)
+}
+
+func executeDeleteTask(conn *Connection, task webTaskData) {
+ done := make(chan bool)
+
+ defer func() {
+ close(done)
+ common.Connections.Remove(conn.GetID())
+ }()
+
+ go keepAliveTask(task, done, 2*time.Minute)
+
+ status := http.StatusOK
+ if err := conn.RemoveAll(task.Path); err != nil {
+ status = getMappedStatusCode(err)
+ }
+
+ task.Timestamp = util.GetTimeAsMsSinceEpoch(time.Now())
+ task.Status = status
+ err := webTaskMgr.Add(task)
+ conn.Log(logger.LevelDebug, "delete task finished, status: %d, update task err: %v", status, err)
+}
+
+func executeRenameTask(conn *Connection, task webTaskData) {
+ done := make(chan bool)
+
+ defer func() {
+ close(done)
+ common.Connections.Remove(conn.GetID())
+ }()
+
+ go keepAliveTask(task, done, 2*time.Minute)
+
+ status := http.StatusOK
+
+ if !conn.IsSameResource(task.Path, task.Target) {
+ if err := conn.Copy(task.Path, task.Target); err != nil {
+ status = getMappedStatusCode(err)
+ task.Timestamp = util.GetTimeAsMsSinceEpoch(time.Now())
+ task.Status = status
+ err = webTaskMgr.Add(task)
+ conn.Log(logger.LevelDebug, "copy step for rename task finished, status: %d, update task err: %v", status, err)
+ return
+ }
+ if err := conn.RemoveAll(task.Path); err != nil {
+ status = getMappedStatusCode(err)
+ }
+ } else {
+ if err := conn.Rename(task.Path, task.Target); err != nil {
+ status = getMappedStatusCode(err)
+ }
+ }
+
+ task.Timestamp = util.GetTimeAsMsSinceEpoch(time.Now())
+ task.Status = status
+ err := webTaskMgr.Add(task)
+ conn.Log(logger.LevelDebug, "rename task finished, status: %d, update task err: %v", status, err)
+}
+
+func executeCopyTask(conn *Connection, task webTaskData) {
+ done := make(chan bool)
+
+ defer func() {
+ close(done)
+ common.Connections.Remove(conn.GetID())
+ }()
+
+ go keepAliveTask(task, done, 2*time.Minute)
+
+ status := http.StatusOK
+ if err := conn.Copy(task.Path, task.Target); err != nil {
+ status = getMappedStatusCode(err)
+ }
+
+ task.Timestamp = util.GetTimeAsMsSinceEpoch(time.Now())
+ task.Status = status
+ err := webTaskMgr.Add(task)
+ conn.Log(logger.LevelDebug, "copy task finished, status: %d, update task err: %v", status, err)
+}
+
+func keepAliveTask(task webTaskData, done chan bool, interval time.Duration) {
+ ticker := time.NewTicker(interval)
+ defer func() {
+ ticker.Stop()
+ }()
+
+ for {
+ select {
+ case <-done:
+ return
+ case <-ticker.C:
+ task.Timestamp = util.GetTimeAsMsSinceEpoch(time.Now())
+ err := webTaskMgr.Add(task)
+ logger.Debug(logSender, task.ID, "task timestamp updated, err: %v", err)
+ }
+ }
+}
diff --git a/internal/httpd/webtask.go b/internal/httpd/webtask.go
new file mode 100644
index 00000000..8375c9c0
--- /dev/null
+++ b/internal/httpd/webtask.go
@@ -0,0 +1,108 @@
+// Copyright (C) 2024 Nicola Murino
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package httpd
+
+import (
+ "encoding/json"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/drakkan/sftpgo/v2/internal/dataprovider"
+ "github.com/drakkan/sftpgo/v2/internal/logger"
+ "github.com/drakkan/sftpgo/v2/internal/util"
+)
+
+var (
+ webTaskMgr webTaskManager
+)
+
+func newWebTaskManager(isShared int) webTaskManager {
+ if isShared == 1 {
+ logger.Info(logSender, "", "using provider task manager")
+ return &dbTaskManager{}
+ }
+ logger.Info(logSender, "", "using memory task manager")
+ return &memoryTaskManager{}
+}
+
+type webTaskManager interface {
+ Add(data webTaskData) error
+ Get(ID string) (webTaskData, error)
+ Cleanup()
+}
+
+type webTaskData struct {
+ ID string `json:"id"`
+ User string `json:"user"`
+ Path string `json:"path"`
+ Target string `json:"target"`
+ Timestamp int64 `json:"ts"`
+ Status int `json:"status"` // 0 in progress or http status code (200 ok, 403 and so on)
+}
+
+type memoryTaskManager struct {
+ tasks sync.Map
+}
+
+func (m *memoryTaskManager) Add(data webTaskData) error {
+ m.tasks.Store(data.ID, &data)
+ return nil
+}
+
+func (m *memoryTaskManager) Get(ID string) (webTaskData, error) {
+ data, ok := m.tasks.Load(ID)
+ if !ok {
+ return webTaskData{}, util.NewRecordNotFoundError(fmt.Sprintf("task for ID %q not found", ID))
+ }
+ return *data.(*webTaskData), nil
+}
+
+func (m *memoryTaskManager) Cleanup() {
+ m.tasks.Range(func(key, value any) bool {
+ data := value.(*webTaskData)
+ if data.Timestamp < util.GetTimeAsMsSinceEpoch(time.Now().Add(-5*time.Minute)) {
+ m.tasks.Delete(key)
+ }
+ return true
+ })
+}
+
+type dbTaskManager struct{}
+
+func (m *dbTaskManager) Add(data webTaskData) error {
+ session := dataprovider.Session{
+ Key: data.ID,
+ Data: data,
+ Type: dataprovider.SessionTypeWebTask,
+ Timestamp: data.Timestamp,
+ }
+ return dataprovider.AddSharedSession(session)
+}
+
+func (m *dbTaskManager) Get(ID string) (webTaskData, error) {
+ sess, err := dataprovider.GetSharedSession(ID)
+ if err != nil {
+ return webTaskData{}, err
+ }
+ d := sess.Data.([]byte)
+ var data webTaskData
+ err = json.Unmarshal(d, &data)
+ return data, err
+}
+
+func (m *dbTaskManager) Cleanup() {
+ dataprovider.CleanupSharedSessions(dataprovider.SessionTypeWebTask, time.Now().Add(-5*time.Minute)) //nolint:errcheck
+}
diff --git a/internal/httpd/webtask_test.go b/internal/httpd/webtask_test.go
new file mode 100644
index 00000000..f4e2b949
--- /dev/null
+++ b/internal/httpd/webtask_test.go
@@ -0,0 +1,133 @@
+// Copyright (C) 2024 Nicola Murino
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published
+// by the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package httpd
+
+import (
+ "testing"
+ "time"
+
+ "github.com/rs/xid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/drakkan/sftpgo/v2/internal/util"
+)
+
+func TestMemoryWebTaskManager(t *testing.T) {
+ mgr := newWebTaskManager(0)
+ m, ok := mgr.(*memoryTaskManager)
+ require.True(t, ok)
+ task := webTaskData{
+ ID: xid.New().String(),
+ User: defeaultUsername,
+ Timestamp: time.Now().Add(-1 * time.Hour).UnixMilli(),
+ Status: 0,
+ }
+ task1 := webTaskData{
+ ID: xid.New().String(),
+ User: defeaultUsername,
+ Timestamp: time.Now().UnixMilli(),
+ Status: 0,
+ }
+ err := m.Add(task)
+ require.NoError(t, err)
+ err = m.Add(task1)
+ require.NoError(t, err)
+ taskGet, err := m.Get(task.ID)
+ require.NoError(t, err)
+ require.Equal(t, task, taskGet)
+ m.Cleanup()
+ _, err = m.Get(task.ID)
+ require.ErrorIs(t, err, util.ErrNotFound)
+ taskGet, err = m.Get(task1.ID)
+ require.NoError(t, err)
+ require.Equal(t, task1, taskGet)
+ task1.Timestamp = time.Now().Add(-1 * time.Hour).UnixMilli()
+ err = m.Add(task1)
+ require.NoError(t, err)
+ m.Cleanup()
+ _, err = m.Get(task.ID)
+ require.ErrorIs(t, err, util.ErrNotFound)
+ // test keep alive task
+ oldMgr := webTaskMgr
+ webTaskMgr = mgr
+
+ done := make(chan bool)
+ go keepAliveTask(task, done, 50*time.Millisecond)
+
+ time.Sleep(120 * time.Millisecond)
+ close(done)
+ taskGet, err = m.Get(task.ID)
+ require.NoError(t, err)
+ assert.Greater(t, taskGet.Timestamp, task.Timestamp)
+ m.Cleanup()
+ _, err = m.Get(task.ID)
+ require.NoError(t, err)
+ err = m.Add(task)
+ require.NoError(t, err)
+ m.Cleanup()
+ _, err = m.Get(task.ID)
+ require.ErrorIs(t, err, util.ErrNotFound)
+
+ webTaskMgr = oldMgr
+}
+
+func TestDbWebTaskManager(t *testing.T) {
+ if !isSharedProviderSupported() {
+ t.Skip("this test it is not available with this provider")
+ }
+ mgr := newWebTaskManager(1)
+ m, ok := mgr.(*dbTaskManager)
+ require.True(t, ok)
+
+ task := webTaskData{
+ ID: xid.New().String(),
+ User: defeaultUsername,
+ Timestamp: time.Now().Add(-1 * time.Hour).UnixMilli(),
+ Status: 0,
+ }
+ err := m.Add(task)
+ require.NoError(t, err)
+ taskGet, err := m.Get(task.ID)
+ require.NoError(t, err)
+ require.Equal(t, task, taskGet)
+ m.Cleanup()
+ _, err = m.Get(task.ID)
+ require.ErrorIs(t, err, util.ErrNotFound)
+ err = m.Add(task)
+ require.NoError(t, err)
+ // test keep alive task
+ oldMgr := webTaskMgr
+ webTaskMgr = mgr
+
+ done := make(chan bool)
+ go keepAliveTask(task, done, 50*time.Millisecond)
+
+ time.Sleep(120 * time.Millisecond)
+ close(done)
+ taskGet, err = m.Get(task.ID)
+ require.NoError(t, err)
+ assert.Greater(t, taskGet.Timestamp, task.Timestamp)
+ m.Cleanup()
+ _, err = m.Get(task.ID)
+ require.NoError(t, err)
+ err = m.Add(task)
+ require.NoError(t, err)
+ m.Cleanup()
+ _, err = m.Get(task.ID)
+ require.ErrorIs(t, err, util.ErrNotFound)
+
+ webTaskMgr = oldMgr
+}
diff --git a/templates/webclient/files.html b/templates/webclient/files.html
index 3fc71f24..6fa538aa 100644
--- a/templates/webclient/files.html
+++ b/templates/webclient/files.html
@@ -264,6 +264,62 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
//{{- end}}
}
+ var taskStatusWaiter = function () {
+ var promiseResolve;
+
+ function getTaskStatus(taskID, numErrors) {
+ axios.get('{{.TasksURL}}'+"/"+encodeURIComponent(taskID),{
+ timeout: 15000,
+ headers: {
+ 'X-CSRF-TOKEN': '{{.CSRFToken}}'
+ },
+ validateStatus: function (status) {
+ return status == 200;
+ }
+ }).then(function(response){
+ let status = response.data.status;
+ if (status == 0){
+ setTimeout(function() {
+ getTaskStatus(taskID, numErrors);
+ }, 2500);
+ } else {
+ promiseResolve({
+ status: status
+ });
+ }
+ }).catch(function(error){
+ numErrors++;
+ if (numErrors >= 3){
+ promiseResolve({
+ status: 0
+ });
+ return;
+ }
+ if (error && error.response && error.response.status == 404){
+ promiseResolve({
+ status: 404
+ });
+ return;
+ }
+ setTimeout(function() {
+ getTaskStatus(taskID, numErrors);
+ }, 2500);
+ });
+ }
+
+ return {
+ wait: function(params){
+ setTimeout(function() {
+ getTaskStatus(params.taskID, 0);
+ }, params.delay);
+
+ return new Promise(function(resolve, reject) {
+ promiseResolve = resolve;
+ });
+ }
+ }
+ }();
+
//{{- if not .ShareUploadBaseURL}}
var KTDatatablesFoldersExplorer = function () {
var dt;
@@ -1048,6 +1104,35 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
const deleteButton = document.querySelector('[data-kt-filemanager-table-select="delete_selected"]');
if (deleteButton) {
+
+ function getMultiDeleteErrorMessage(status, deleted) {
+ let errorMessage;
+ switch (status) {
+ case 403:
+ if (deleted > 0){
+ errorMessage = "fs.delete_multi.err_403_partial";
+ } else {
+ errorMessage = "fs.delete_multi.err_403";
+ }
+ break;
+ case 429:
+ if (deleted > 0){
+ errorMessage = "fs.delete_multi.err_429_partial";
+ } else {
+ errorMessage = "fs.delete_multi.err_429";
+ }
+ break;
+ }
+ if (!errorMessage){
+ if (deleted > 0){
+ errorMessage = "fs.delete_multi.err_generic_partial";
+ } else {
+ errorMessage = "fs.delete_multi.err_generic";
+ }
+ }
+ return errorMessage;
+ }
+
let el = $(deleteButton);
el.off("click");
el.on('click', function(e){
@@ -1075,14 +1160,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (selectedRowsIdx.length == 0){
return;
}
- keepAlive();
- let keepAliveTimer = setInterval(keepAlive, 300000);
$('#loading_message').text("");
KTApp.showPageLoading();
function deleteSelected() {
if (index >= selectedRowsIdx.length || hasError){
- clearInterval(keepAliveTimer);
KTApp.hidePageLoading();
if (!hasError){
location.reload();
@@ -1090,57 +1172,66 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
return;
}
let meta = dt.row(selectedRowsIdx[index]).data()['meta'];
- let attrs = getDeleteReqAttrs(meta);
+ let itemName = getNameFromMeta(meta);
+ let isDir = (getTypeFromMeta(meta) == "1");
+ let path;
+ if (isDir){
+ path = '{{.DirsURL}}';
+ } else {
+ path = '{{.FilesURL}}';
+ }
+ path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+itemName);
+
let deleteTxt = "";
if (selectedRowsIdx.length > 1){
deleteTxt = $.t('fs.deleting', {
idx : index + 1,
total: selectedRowsIdx.length,
- name: getNameFromMeta(meta)
+ name: itemName
});
}
$('#loading_message').text(deleteTxt);
- axios.delete(attrs.path,{
- timeout: attrs.reqTimeout,
+ axios.delete(path,{
+ timeout: 15000,
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
+ if (isDir){
+ return status == 202;
+ }
return status == 200;
}
}).then(function(response){
- index++;
- deleted++;
- deleteSelected();
+ if (isDir){
+ taskStatusWaiter.wait({
+ taskID: response.data.message,
+ delay: 500
+ }).then((result) => {
+ index++;
+ if (result.status == 200){
+ deleted++;
+ } else {
+ hasError = true;
+ let errorMessage = getMultiDeleteErrorMessage(result.status, deleted);
+ setI18NData(errTxtEl, errorMessage);
+ errDivEl.removeClass("d-none");
+ }
+ deleteSelected();
+ });
+ } else {
+ index++;
+ deleted++;
+ deleteSelected();
+ }
}).catch(function(error){
index++;
hasError = true;
- let errorMessage;
+ let status = 0;
if (error && error.response) {
- switch (error.response.status) {
- case 403:
- if (deleted > 0){
- errorMessage = "fs.delete_multi.err_403_partial";
- } else {
- errorMessage = "fs.delete_multi.err_403";
- }
- break;
- case 429:
- if (deleted > 0){
- errorMessage = "fs.delete_multi.err_429_partial";
- } else {
- errorMessage = "fs.delete_multi.err_429";
- }
- break;
- }
- }
- if (!errorMessage){
- if (deleted > 0){
- errorMessage = "fs.delete_multi.err_generic_partial";
- } else {
- errorMessage = "fs.delete_multi.err_generic";
- }
+ status = error.response.status;
}
+ let errorMessage = getMultiDeleteErrorMessage(status, deleted);
setI18NData(errTxtEl, errorMessage);
errDivEl.removeClass("d-none");
deleteSelected();
@@ -1274,8 +1365,30 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
});
return;
}
- keepAlive();
- let keepAliveTimer = setInterval(keepAlive, 300000);
+
+ function showCopyError(status) {
+ KTApp.hidePageLoading();
+ let errorMessage = "";
+ switch (status) {
+ case 403:
+ errorMessage = "fs.copy.err_403";
+ break;
+ case 429:
+ errorMessage = "fs.copy.err_429";
+ break;
+ default:
+ errorMessage = "fs.copy.err_generic";
+ }
+ ModalAlert.fire({
+ text: $.t(errorMessage),
+ icon: "warning",
+ confirmButtonText: $.t('general.ok'),
+ customClass: {
+ confirmButton: "btn btn-primary"
+ }
+ });
+ }
+
let hasError = false;
let index = 0;
@@ -1284,7 +1397,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
function copyItem() {
if (index >= items.length || hasError){
- clearInterval(keepAliveTimer);
KTApp.hidePageLoading();
if (!hasError){
location.reload();
@@ -1316,41 +1428,33 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+item.sourceName)+'&target='+item.targetDir+encodeURIComponent("/"+item.targetName);
axios.post(path, null, {
- timeout: 180000,
+ timeout: 15000,
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
- return status == 200;
+ return status == 202;
}
}).then(function (response) {
- index++;
- copyItem();
+ taskStatusWaiter.wait({
+ taskID: response.data.message,
+ delay: 500
+ }).then((result) => {
+ index++;
+ if (result.status != 200){
+ hasError = true;
+ showCopyError(result.status);
+ }
+ copyItem();
+ });
}).catch(function (error) {
index++;
hasError = true;
- let errorMessage = "";
+ let status = 0;
if (error && error.response) {
- switch (error.response.status) {
- case 403:
- errorMessage = "fs.copy.err_403";
- break;
- case 429:
- errorMessage = "fs.copy.err_429";
- break;
- }
+ status = error.response.status;
}
- if (!errorMessage){
- errorMessage = "fs.copy.err_generic";
- }
- ModalAlert.fire({
- text: $.t(errorMessage),
- icon: "warning",
- confirmButtonText: $.t('general.ok'),
- customClass: {
- confirmButton: "btn btn-primary"
- }
- });
+ showCopyError(status);
copyItem();
});
}
@@ -1410,8 +1514,33 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
});
return;
}
- keepAlive();
- let keepAliveTimer = setInterval(keepAlive, 300000);
+
+ function showMoveError(status) {
+ KTApp.hidePageLoading();
+ let errorMessage = "";
+ switch (status) {
+ case 400:
+ errorMessage = "fs.move.err_unsupported";
+ break;
+ case 403:
+ errorMessage = "fs.move.err_403";
+ break;
+ case 429:
+ errorMessage = "fs.move.err_429";
+ break;
+ default:
+ errorMessage = "fs.move.err_generic";
+ }
+ ModalAlert.fire({
+ text: $.t(errorMessage),
+ icon: "warning",
+ confirmButtonText: $.t('general.ok'),
+ customClass: {
+ confirmButton: "btn btn-primary"
+ }
+ });
+ }
+
let hasError = false;
let index = 0;
@@ -1420,7 +1549,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
function moveItem() {
if (index >= items.length || hasError){
- clearInterval(keepAliveTimer);
KTApp.hidePageLoading();
if (!hasError){
location.reload();
@@ -1452,44 +1580,33 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+item.sourceName)+'&target='+item.targetDir+encodeURIComponent("/"+item.targetName);
axios.post(path, null, {
- timeout: 180000,
+ timeout: 15000,
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
- return status == 200;
+ return status == 202;
}
}).then(function (response) {
- index++;
- moveItem();
+ taskStatusWaiter.wait({
+ taskID: response.data.message,
+ delay: 500
+ }).then((result) => {
+ index++;
+ if (result.status != 200){
+ hasError = true;
+ showMoveError(result.status);
+ }
+ moveItem();
+ });
}).catch(function (error) {
index++;
hasError = true;
- let errorMessage = "";
+ let status = 0;
if (error && error.response) {
- switch (error.response.status) {
- case 400:
- errorMessage = "fs.move.err_unsupported";
- break;
- case 403:
- errorMessage = "fs.move.err_403";
- break;
- case 429:
- errorMessage = "fs.move.err_429";
- break;
- }
+ status = error.response.status;
}
- if (!errorMessage){
- errorMessage = "fs.move.err_generic";
- }
- ModalAlert.fire({
- text: $.t(errorMessage),
- icon: "warning",
- confirmButtonText: $.t('general.ok'),
- customClass: {
- confirmButton: "btn btn-primary"
- }
- });
+ showMoveError(status);
moveItem();
});
}
@@ -1531,20 +1648,27 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
});
}
- function getDeleteReqAttrs(meta) {
- let path;
- let reqTimeout = 15000;
- let itemType = getTypeFromMeta(meta);
- let itemName = getNameFromMeta(meta);
- if (itemType == "1"){
- path = '{{.DirsURL}}';
- reqTimeout = 120000
- } else {
- path = '{{.FilesURL}}';
+ function showDeleteItemError(status, itemName) {
+ KTApp.hidePageLoading();
+ let errorMessage;
+ switch (status) {
+ case 403:
+ errorMessage = "fs.delete.err_403";
+ break;
+ case 429:
+ errorMessage = "fs.delete.err_429";
+ break;
+ default:
+ errorMessage = "fs.delete.err_generic";
}
- path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+itemName);
-
- return { path, reqTimeout}
+ ModalAlert.fire({
+ text: $.t(errorMessage, {name: itemName}),
+ icon: "warning",
+ confirmButtonText: $.t('general.ok'),
+ customClass: {
+ confirmButton: "btn btn-primary"
+ }
+ });
}
function deleteItem(meta) {
@@ -1564,42 +1688,47 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (result.isConfirmed){
$('#loading_message').text("");
KTApp.showPageLoading();
- let attrs = getDeleteReqAttrs(meta);
+ let isDir = (getTypeFromMeta(meta) == "1");
+ let path;
+ if (isDir){
+ path = '{{.DirsURL}}';
+ } else {
+ path = '{{.FilesURL}}';
+ }
+ path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+itemName);
- axios.delete(attrs.path, {
- timeout: attrs.reqTimeout,
+ axios.delete(path, {
+ timeout: 15000,
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
+ if (isDir){
+ return status == 202;
+ }
return status == 200;
}
}).then(function(response){
- location.reload();
+ if (isDir){
+ taskStatusWaiter.wait({
+ taskID: response.data.message,
+ delay: 500
+ }).then((result) => {
+ if (result.status == 200){
+ location.reload();
+ } else {
+ showDeleteItemError(result.status, itemName);
+ }
+ });
+ } else {
+ location.reload();
+ }
}).catch(function(error){
- KTApp.hidePageLoading();
- let errorMessage;
+ let status = 0;
if (error && error.response) {
- switch (error.response.status) {
- case 403:
- errorMessage = "fs.delete.err_403";
- break;
- case 429:
- errorMessage = "fs.delete.err_429";
- break;
- }
+ status = error.response.status;
}
- if (!errorMessage){
- errorMessage = "fs.delete.err_generic";
- }
- ModalAlert.fire({
- text: $.t(errorMessage, {name: itemName}),
- icon: "warning",
- confirmButtonText: $.t('general.ok'),
- customClass: {
- confirmButton: "btn btn-primary"
- }
- });
+ showDeleteItemError(status, itemName);
});
}
});
@@ -1624,6 +1753,33 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
$('#modal_rename').modal('show');
}
+ function showRenameItemError(status, oldName) {
+ KTApp.hidePageLoading();
+ let errorMessage;
+ switch (status) {
+ case 400:
+ errorMessage = "fs.rename.err_unsupported";
+ break;
+ case 403:
+ errorMessage = "fs.rename.err_403";
+ break;
+ case 429:
+ errorMessage = "fs.rename.err_429";
+ break;
+ default:
+ errorMessage = "fs.rename.err_generic";
+ }
+
+ ModalAlert.fire({
+ text: $.t(errorMessage, {name: oldName}),
+ icon: "warning",
+ confirmButtonText: $.t('general.ok'),
+ customClass: {
+ confirmButton: "btn btn-primary"
+ }
+ });
+ }
+
function doRename() {
let meta = $('#rename_old_name').val();
let oldName = getNameFromMeta(meta);
@@ -1674,37 +1830,25 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
- return status == 200;
+ return status == 202;
}
}).then(function (response) {
- location.reload();
- }).catch(function (error) {
- KTApp.hidePageLoading();
- let errorMessage;
- if (error && error.response) {
- switch (error.response.status) {
- case 400:
- errorMessage = "fs.rename.err_unsupported";
- break;
- case 403:
- errorMessage = "fs.rename.err_403";
- break;
- case 429:
- errorMessage = "fs.rename.err_429";
- break;
+ taskStatusWaiter.wait({
+ taskID: response.data.message,
+ delay: 500
+ }).then((result) => {
+ if (result.status == 200){
+ location.reload();
+ } else {
+ showRenameItemError(result.status, oldName);
}
+ });
+ }).catch(function (error) {
+ let status = 0;
+ if (error && error.response) {
+ status = error.response.status;
}
- if (!errorMessage) {
- errorMessage = "fs.rename.err_generic";
- }
- ModalAlert.fire({
- text: $.t(errorMessage, {name: oldName}),
- icon: "warning",
- confirmButtonText: $.t('general.ok'),
- customClass: {
- confirmButton: "btn btn-primary"
- }
- });
+ showRenameItemError(status, oldName);
});
}