WebClient: refactor long-running tasks to improve browser compatibility

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-03-11 18:19:57 +01:00
parent baaef63d1d
commit f38966c6ac
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
12 changed files with 1118 additions and 243 deletions

58
go.mod
View file

@ -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

108
go.sum
View file

@ -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=

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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"

View file

@ -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) {

View file

@ -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)

View file

@ -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)
}
}
}

108
internal/httpd/webtask.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
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
}

View file

@ -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 <https://www.gnu.org/licenses/>.
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
}

View file

@ -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);
});
}