shares: add an upload form for shares with write scope
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
ebbbf81e65
commit
f1832d4478
8 changed files with 253 additions and 36 deletions
10
go.mod
10
go.mod
|
@ -7,7 +7,7 @@ require (
|
|||
github.com/Azure/azure-storage-blob-go v0.14.0
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/aws/aws-sdk-go v1.42.52
|
||||
github.com/aws/aws-sdk-go v1.42.53
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.8
|
||||
github.com/coreos/go-oidc/v3 v3.1.0
|
||||
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
|
||||
|
@ -53,7 +53,7 @@ require (
|
|||
go.etcd.io/bbolt v1.3.6
|
||||
go.uber.org/automaxprocs v1.4.0
|
||||
gocloud.dev v0.24.0
|
||||
golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
|
||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158
|
||||
|
@ -64,8 +64,8 @@ require (
|
|||
|
||||
require (
|
||||
cloud.google.com/go v0.100.2 // indirect
|
||||
cloud.google.com/go/compute v1.2.0 // indirect
|
||||
cloud.google.com/go/iam v0.1.1 // indirect
|
||||
cloud.google.com/go/compute v1.3.0 // indirect
|
||||
cloud.google.com/go/iam v0.2.0 // indirect
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
|
@ -139,6 +139,6 @@ require (
|
|||
replace (
|
||||
github.com/eikenb/pipeat => github.com/drakkan/pipeat v0.0.0-20210805162858-70e57fa8a639
|
||||
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220130095207-a206cf284b7c
|
||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220215181150-74469fa99b22
|
||||
golang.org/x/net => github.com/drakkan/net v0.0.0-20220130095023-bd85f1236c34
|
||||
)
|
||||
|
|
16
go.sum
16
go.sum
|
@ -46,14 +46,16 @@ cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUM
|
|||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
|
||||
cloud.google.com/go/compute v1.2.0 h1:EKki8sSdvDU0OO9mAXGwPXOTOgPz2l08R0/IutDH11I=
|
||||
cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw=
|
||||
cloud.google.com/go/compute v1.3.0 h1:mPL/MzDDYHsh5tHRS9mhmhWlcgClCrCa6ApQCU6wnHI=
|
||||
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.5.0/go.mod h1:c4nNYR1qdq7eaZ+jSc5fonrQN2k3M7sWATcYTiakjEo=
|
||||
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
|
||||
cloud.google.com/go/iam v0.1.1 h1:4CapQyNFjiksks1/x7jsvsygFPhihslYk5GptIrlX68=
|
||||
cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw=
|
||||
cloud.google.com/go/iam v0.2.0 h1:Ouq6qif4mZdXkb3SiFMpxvu0JQJB1Yid9TsZ23N6hg8=
|
||||
cloud.google.com/go/iam v0.2.0/go.mod h1:BCK88+tmjAwnZYfOSizmKCTSFjJHCa18t3DpdGEY13Y=
|
||||
cloud.google.com/go/kms v0.1.0 h1:VXAb5OzejDcyhFzIDeZ5n5AUdlsFnCyexuascIwWMj0=
|
||||
cloud.google.com/go/kms v0.1.0/go.mod h1:8Qp8PCAypHg4FdmlyW1QRAv09BGQ9Uzh7JnmIZxPk+c=
|
||||
cloud.google.com/go/monitoring v0.1.0/go.mod h1:Hpm3XfzJv+UTiXzCG5Ffp0wijzHTC7Cv4eR7o3x/fEE=
|
||||
|
@ -141,8 +143,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
|
|||
github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
||||
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.40.34/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go v1.42.52 h1:/+TZ46+0qu9Ph/UwjVrU3SG8OBi87uJLrLiYRNZKbHQ=
|
||||
github.com/aws/aws-sdk-go v1.42.52/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc=
|
||||
github.com/aws/aws-sdk-go v1.42.53 h1:56T04NWcmc0ZVYFbUc6HdewDQ9iHQFlmS6hj96dRjJs=
|
||||
github.com/aws/aws-sdk-go v1.42.53/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.7.0/go.mod h1:w9+nMZ7soXCe5nT46Ri354SNhXDQ6v+V5wqDjnZE+GY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.4.0/go.mod h1:dgGR+Qq7Wjcd4AOAW5Rf5Tnv3+x7ed6kETXyS9WCuAY=
|
||||
|
@ -216,8 +218,8 @@ github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mz
|
|||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||
github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
|
||||
github.com/drakkan/crypto v0.0.0-20220130095207-a206cf284b7c h1:IqTZK/MGRdMPRyyJQSxDtrEokSJDJl+nreM2/CFYTsg=
|
||||
github.com/drakkan/crypto v0.0.0-20220130095207-a206cf284b7c/go.mod h1:SiM6ypd8Xu1xldObYtbDztuUU7xUzMnUULfphXFZmro=
|
||||
github.com/drakkan/crypto v0.0.0-20220215181150-74469fa99b22 h1:yHFyJbCfvTY65bTyPOMRGqplA5GNfdZhBVOiYGNeCtY=
|
||||
github.com/drakkan/crypto v0.0.0-20220215181150-74469fa99b22/go.mod h1:SiM6ypd8Xu1xldObYtbDztuUU7xUzMnUULfphXFZmro=
|
||||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
|
||||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
|
||||
github.com/drakkan/net v0.0.0-20220130095023-bd85f1236c34 h1:DRayAKtBRaVU3jg58b/HCbkRleByBD5q6NkN1wcJ2RU=
|
||||
|
@ -1082,6 +1084,7 @@ google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFd
|
|||
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
|
||||
google.golang.org/api v0.64.0/go.mod h1:931CdxA8Rm4t6zqTFGSsgwbAEZ2+GMYurbndwSimebM=
|
||||
google.golang.org/api v0.66.0/go.mod h1:I1dmXYpX7HGwz/ejRxwQp2qj5bFAz93HiCU1C1oYd9M=
|
||||
google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
|
||||
google.golang.org/api v0.68.0 h1:9eJiHhwJKIYX6sX2fUZxQLi7pDRA/MYu8c12q6WbJik=
|
||||
google.golang.org/api v0.68.0/go.mod h1:sOM8pTpwgflXRhz+oC8H2Dr+UcbMqkPPWNJo88Q7TH8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
|
@ -1171,6 +1174,7 @@ google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ6
|
|||
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220204002441-d6cc3cc0770e/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220211171837-173942840c17 h1:2X+CNIheCutWRyKRte8szGxrE5ggtV4U+NKAbh/oLhg=
|
||||
google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
|
|
|
@ -138,7 +138,7 @@ func deleteShare(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func readBrowsableShareContents(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -165,7 +165,7 @@ func readBrowsableShareContents(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -211,7 +211,7 @@ func downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func downloadFromShare(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -263,7 +263,7 @@ func uploadFileToShare(w http.ResponseWriter, r *http.Request) {
|
|||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
|
||||
}
|
||||
name := getURLParam(r, "name")
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeWrite)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeWrite, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -285,7 +285,7 @@ func uploadFilesToShare(w http.ResponseWriter, r *http.Request) {
|
|||
if maxUploadFileSize > 0 {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
|
||||
}
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeWrite)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeWrite, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -332,40 +332,53 @@ func uploadFilesToShare(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func checkPublicShare(w http.ResponseWriter, r *http.Request, shareShope dataprovider.ShareScope,
|
||||
isWebClient bool,
|
||||
) (dataprovider.Share, *Connection, error) {
|
||||
renderError := func(err error, message string, statusCode int) {
|
||||
if isWebClient {
|
||||
renderClientMessagePage(w, r, "Unable to access the share", message, statusCode, err, "")
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, message, statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
shareID := getURLParam(r, "id")
|
||||
share, err := dataprovider.ShareExists(shareID, "")
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
statusCode := getRespStatus(err)
|
||||
if statusCode == http.StatusNotFound {
|
||||
err = errors.New("share does not exist")
|
||||
}
|
||||
renderError(err, "", statusCode)
|
||||
return share, nil, err
|
||||
}
|
||||
if share.Scope != shareShope {
|
||||
sendAPIResponse(w, r, nil, "Invalid share scope", http.StatusForbidden)
|
||||
renderError(nil, "Invalid share scope", http.StatusForbidden)
|
||||
return share, nil, errors.New("invalid share scope")
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
ok, err := share.IsUsable(ipAddr)
|
||||
if !ok || err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return share, nil, errors.New("login not allowed")
|
||||
renderError(err, "", getRespStatus(err))
|
||||
return share, nil, err
|
||||
}
|
||||
if share.Password != "" {
|
||||
_, password, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
|
||||
sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
renderError(dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return share, nil, dataprovider.ErrInvalidCredentials
|
||||
}
|
||||
match, err := share.CheckPassword(password)
|
||||
if !match || err != nil {
|
||||
w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
|
||||
sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
renderError(dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return share, nil, dataprovider.ErrInvalidCredentials
|
||||
}
|
||||
}
|
||||
user, err := dataprovider.UserExists(share.Username)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
renderError(err, "", getRespStatus(err))
|
||||
return share, nil, err
|
||||
}
|
||||
connID := xid.New().String()
|
||||
|
|
|
@ -9412,6 +9412,12 @@ func TestShareUploadSingle(t *testing.T) {
|
|||
if assert.NoError(t, err) {
|
||||
assert.InDelta(t, util.GetTimeAsMsSinceEpoch(modTime), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(1000))
|
||||
}
|
||||
req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "upload"), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, "file.txt"), bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
|
@ -9668,6 +9674,12 @@ func TestBrowseShares(t *testing.T) {
|
|||
objectID := rr.Header().Get("X-Object-ID")
|
||||
assert.NotEmpty(t, objectID)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "upload"), nil)
|
||||
assert.NoError(t, err)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid share scope")
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "browse?path=%2F"), nil)
|
||||
assert.NoError(t, err)
|
||||
rr = executeRequest(req)
|
||||
|
|
|
@ -1306,8 +1306,9 @@ func (s *httpdServer) setupWebClientRoutes() {
|
|||
Post(webClientTwoFactorRecoveryPath, s.handleWebClientTwoFactorRecoveryPost)
|
||||
// share API exposed to external users
|
||||
s.router.Get(webClientPubSharesPath+"/{id}", downloadFromShare)
|
||||
s.router.Get(webClientPubSharesPath+"/{id}/browse", s.handleShareGetFiles)
|
||||
s.router.With(compressor.Handler).Get(webClientPubSharesPath+"/{id}/dirs", s.handleShareGetDirContents)
|
||||
s.router.Get(webClientPubSharesPath+"/{id}/browse", handleShareGetFiles)
|
||||
s.router.Get(webClientPubSharesPath+"/{id}/upload", handleClientUploadToShare)
|
||||
s.router.With(compressor.Handler).Get(webClientPubSharesPath+"/{id}/dirs", handleShareGetDirContents)
|
||||
s.router.Post(webClientPubSharesPath+"/{id}", uploadFilesToShare)
|
||||
s.router.Post(webClientPubSharesPath+"/{id}/{name}", uploadFileToShare)
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ const (
|
|||
templateClientShares = "shares.html"
|
||||
templateClientViewPDF = "viewpdf.html"
|
||||
templateShareFiles = "sharefiles.html"
|
||||
templateUploadToShare = "shareupload.html"
|
||||
pageClientFilesTitle = "My Files"
|
||||
pageClientSharesTitle = "Shares"
|
||||
pageClientProfileTitle = "My Profile"
|
||||
|
@ -55,6 +56,7 @@ const (
|
|||
pageClientForgotPwdTitle = "SFTPGo WebClient - Forgot password"
|
||||
pageClientResetPwdTitle = "SFTPGo WebClient - Reset password"
|
||||
pageExtShareTitle = "Shared files"
|
||||
pageUploadToShareTitle = "Upload to share"
|
||||
)
|
||||
|
||||
// condResult is the result of an HTTP request precondition check.
|
||||
|
@ -147,6 +149,12 @@ type shareFilesPage struct {
|
|||
Paths []dirMapping
|
||||
}
|
||||
|
||||
type shareUploadPage struct {
|
||||
baseClientPage
|
||||
Share *dataprovider.Share
|
||||
UploadBasePath string
|
||||
}
|
||||
|
||||
type clientMessagePage struct {
|
||||
baseClientPage
|
||||
Error string
|
||||
|
@ -261,6 +269,10 @@ func loadClientTemplates(templatesPath string) {
|
|||
filepath.Join(templatesPath, templateClientDir, templateClientBase),
|
||||
filepath.Join(templatesPath, templateClientDir, templateShareFiles),
|
||||
}
|
||||
shareUploadPath := []string{
|
||||
filepath.Join(templatesPath, templateClientDir, templateClientBase),
|
||||
filepath.Join(templatesPath, templateClientDir, templateUploadToShare),
|
||||
}
|
||||
|
||||
filesTmpl := util.LoadTemplate(nil, filesPaths...)
|
||||
profileTmpl := util.LoadTemplate(nil, profilePaths...)
|
||||
|
@ -277,6 +289,7 @@ func loadClientTemplates(templatesPath string) {
|
|||
resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
|
||||
viewPDFTmpl := util.LoadTemplate(nil, viewPDFPaths...)
|
||||
shareFilesTmpl := util.LoadTemplate(nil, shareFilesPath...)
|
||||
shareUploadTmpl := util.LoadTemplate(nil, shareUploadPath...)
|
||||
|
||||
clientTemplates[templateClientFiles] = filesTmpl
|
||||
clientTemplates[templateClientProfile] = profileTmpl
|
||||
|
@ -293,6 +306,7 @@ func loadClientTemplates(templatesPath string) {
|
|||
clientTemplates[templateResetPassword] = resetPwdTmpl
|
||||
clientTemplates[templateClientViewPDF] = viewPDFTmpl
|
||||
clientTemplates[templateShareFiles] = shareFilesTmpl
|
||||
clientTemplates[templateUploadToShare] = shareUploadTmpl
|
||||
}
|
||||
|
||||
func getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage {
|
||||
|
@ -495,6 +509,16 @@ func renderSharedFilesPage(w http.ResponseWriter, r *http.Request, dirName, erro
|
|||
renderClientTemplate(w, templateShareFiles, data)
|
||||
}
|
||||
|
||||
func renderUploadToSharePage(w http.ResponseWriter, r *http.Request, share dataprovider.Share) {
|
||||
currentURL := path.Join(webClientPubSharesPath, share.ShareID, "upload")
|
||||
data := shareUploadPage{
|
||||
baseClientPage: getBaseClientPageData(pageUploadToShareTitle, currentURL, r),
|
||||
Share: &share,
|
||||
UploadBasePath: path.Join(webClientPubSharesPath, share.ShareID),
|
||||
}
|
||||
renderClientTemplate(w, templateUploadToShare, data)
|
||||
}
|
||||
|
||||
func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user dataprovider.User,
|
||||
hasIntegrations bool,
|
||||
) {
|
||||
|
@ -590,9 +614,9 @@ func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
|
|||
renderCompressedFiles(w, connection, name, filesList, nil)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.Request) {
|
||||
func handleShareGetDirContents(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -636,9 +660,18 @@ func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.R
|
|||
render.JSON(w, r, results)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request) {
|
||||
func handleClientUploadToShare(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead)
|
||||
share, _, err := checkPublicShare(w, r, dataprovider.ShareScopeWrite, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
renderUploadToSharePage(w, r, share)
|
||||
}
|
||||
|
||||
func handleShareGetFiles(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -93,16 +93,24 @@
|
|||
<div class="modal-body">
|
||||
<div id="readShare">
|
||||
<p>You can download the shared contents, as single zip file, using this <a id="readLink" href="#" target="_blank">link</a>.</p>
|
||||
<p>If the share consists of a single folder you can browse and download single files using this <a id="readBrowseLink" href="#" target="_blank">link</a>.</p>
|
||||
<p>If the share consists of a single directory you can browse and download files using this <a id="readBrowseLink" href="#" target="_blank">page</a>.</p>
|
||||
<p>If the share consists of a single file you can download it uncompressed using this <a id="readUncompressedLink" href="#" target="_blank">link</a>.</p>
|
||||
</div>
|
||||
<div id="writeShare">
|
||||
<p>You can upload one or more files to the shared directory by sending a multipart/form-data request to this <a id="writeLink" href="#" target="_blank">link</a>. The form field name for the file(s) is <b><code>filenames</code></b>.</p>
|
||||
<p>For example:</p>
|
||||
<p><code>curl -F filenames=@file1.txt -F filenames=@file2.txt "share link"</code></p>
|
||||
<p>You can upload files one by one by adding the path encoded file name to the share <a id="writeLinkSingle" href="#" target="_blank">link</a> and sending the file as POST body. The optional <b><code>X-SFTPGO-MTIME</code></b> header allows to set the file modification time as milliseconds since epoch.</p>
|
||||
<p>For example:</p>
|
||||
<p><code>curl --data-binary @file.txt -H "Content-Type: application/octet-stream" -H "X-SFTPGO-MTIME: 1638882991234" "share link/file.txt"</code></p>
|
||||
<p>You can upload one or more files to the shared directory using this <a id="writePageLink" href="#" target="_blank">page</a></p>
|
||||
<p>
|
||||
<a data-toggle="collapse" href="#collapseWriteShareAdvanced" aria-expanded="false" aria-controls="collapseWriteShareAdvanced">
|
||||
Advanced options
|
||||
</a>
|
||||
</p>
|
||||
<div class="collapse" id="collapseWriteShareAdvanced">
|
||||
<div class="card card-body">
|
||||
<p>You can upload one or more files to the shared directory by sending a multipart/form-data request to this <a id="writeLink" href="#" target="_blank">link</a>. The form field name for the file(s) is <b><code>filenames</code></b>.</p>
|
||||
<p>Example: <code>curl -F filenames=@file1.txt -F filenames=@file2.txt "share link"</code></p>
|
||||
<p>Or you can upload files one by one by adding the path encoded file name to the share <a id="writeLinkSingle" href="#" target="_blank">link</a> and sending the file as POST body. The optional <b><code>X-SFTPGO-MTIME</code></b> header allows to set the file modification time as milliseconds since epoch.</p>
|
||||
<p>Example: <code>curl --data-binary @file.txt -H "Content-Type: application/octet-stream" -H "X-SFTPGO-MTIME: 1638882991234" "share link/file.txt"</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="expiredShare">
|
||||
This share is no longer accessible because it has expired
|
||||
|
@ -226,6 +234,8 @@
|
|||
$('#expiredShare').hide();
|
||||
$('#writeShare').show();
|
||||
$('#readShare').hide();
|
||||
$('#writePageLink').attr("href", shareURL+"/upload");
|
||||
$('#writePageLink').attr("title", shareURL+"/upload");
|
||||
$('#writeLink').attr("href", shareURL);
|
||||
$('#writeLink').attr("title", shareURL);
|
||||
$('#writeLinkSingle').attr("href", shareURL);
|
||||
|
|
144
templates/webclient/shareupload.html
Normal file
144
templates/webclient/shareupload.html
Normal file
|
@ -0,0 +1,144 @@
|
|||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{.Title}}{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-xl-5 col-lg-6 col-md-8">
|
||||
<div class="card shadow-lg my-5">
|
||||
<div class="card-header py-3">
|
||||
<h6 id="default_title" class="m-0 font-weight-bold text-primary">Upload one or more files to share "{{.Share.Name}}", user "{{.Share.Username}}"</h6>
|
||||
<h6 id="success_title" class="m-0 font-weight-bold text-primary" style="display: none;">Upload completed to share "{{.Share.Name}}", user "{{.Share.Username}}"</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="errorMsg" class="card mb-4 border-left-warning" style="display: none;">
|
||||
<div id="errorTxt" class="card-body text-form-error"></div>
|
||||
</div>
|
||||
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
|
||||
<div id="successTxt" class="card-body">
|
||||
<p>File/s uploaded successfully</p>
|
||||
<p>If you want to upload other files click <a href="javascript:refreshPage();">here</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<form id="upload_files_form" action="#" method="POST" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<input type="file" class="form-control-file" id="files_name" name="filenames" required multiple>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary float-right mt-3 px-5">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "dialog"}}
|
||||
<div class="modal fade" id="spinnerModal" tabindex="-1" role="dialog" data-keyboard="false" data-backdrop="static">
|
||||
<div class="modal-dialog modal-dialog-centered justify-content-center" role="document">
|
||||
<span style="color: #333333;" class="fa fa-spinner fa-spin fa-3x"></span>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "extra_js"}}
|
||||
<script type="text/javascript">
|
||||
var spinnerDone = false;
|
||||
|
||||
function refreshPage() {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$('#spinnerModal').on('shown.bs.modal', function () {
|
||||
if (spinnerDone){
|
||||
$('#spinnerModal').modal('hide');
|
||||
}
|
||||
});
|
||||
|
||||
$("#upload_files_form").submit(function (event){
|
||||
event.preventDefault();
|
||||
var files = $("#files_name")[0].files;
|
||||
var has_errors = false;
|
||||
var index = 0;
|
||||
var success = 0;
|
||||
spinnerDone = false;
|
||||
|
||||
$('#spinnerModal').modal('show');
|
||||
|
||||
function uploadFile() {
|
||||
if (index >= files.length || has_errors){
|
||||
$('#spinnerModal').modal('hide');
|
||||
spinnerDone = true;
|
||||
if (!has_errors){
|
||||
$('#errorMsg').hide();
|
||||
$('#upload_files_form').hide();
|
||||
$('#default_title').hide();
|
||||
$('#success_title').show();
|
||||
$('#successMsg').show();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async function saveFile() {
|
||||
var errorMessage = "Error uploading files";
|
||||
let response;
|
||||
try {
|
||||
var f = files[index];
|
||||
var uploadPath = '{{.UploadBasePath}}/'+fixedEncodeURIComponent(f.name);
|
||||
var lastModified;
|
||||
try {
|
||||
lastModified = f.lastModified;
|
||||
} catch (e) {
|
||||
console.log("unable to get last modified time from file: "+e.message);
|
||||
lastModified = "";
|
||||
}
|
||||
response = await fetch(uploadPath, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-SFTPGO-MTIME': lastModified
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
redirect: 'error',
|
||||
body: f
|
||||
});
|
||||
} catch (e){
|
||||
throw Error(errorMessage+": " +e.message);
|
||||
}
|
||||
if (response.status == 201){
|
||||
index++;
|
||||
success++;
|
||||
uploadFile();
|
||||
} else {
|
||||
let jsonResponse;
|
||||
try {
|
||||
jsonResponse = await response.json();
|
||||
} catch(e){
|
||||
throw Error(errorMessage);
|
||||
}
|
||||
if (jsonResponse.message) {
|
||||
errorMessage = jsonResponse.message;
|
||||
}
|
||||
if (jsonResponse.error) {
|
||||
errorMessage += ": " + jsonResponse.error;
|
||||
}
|
||||
throw Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
saveFile().catch(function(error){
|
||||
index++;
|
||||
has_errors = true;
|
||||
$('#errorTxt').text(error.message);
|
||||
$('#errorMsg').show();
|
||||
setTimeout(function () {
|
||||
$('#errorMsg').hide();
|
||||
}, 10000);
|
||||
uploadFile();
|
||||
});
|
||||
}
|
||||
|
||||
uploadFile();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
Loading…
Reference in a new issue