shares: add an upload form for shares with write scope

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-02-15 19:19:25 +01:00
parent ebbbf81e65
commit f1832d4478
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
8 changed files with 253 additions and 36 deletions

10
go.mod
View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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}}