web client: add share mode read/write
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
e72bb1e124
commit
1e0b3a2a8c
22 changed files with 457 additions and 108 deletions
|
@ -1,7 +1,9 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
@ -25,7 +27,7 @@ current directory.
|
|||
Run: func(cmd *cobra.Command, args []string) {
|
||||
logger.DisableLogger()
|
||||
logger.EnableConsoleLogger(zerolog.DebugLevel)
|
||||
if _, err := os.Stat(manDir); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(manDir); errors.Is(err, fs.ErrNotExist) {
|
||||
err = os.MkdirAll(manDir, os.ModePerm)
|
||||
if err != nil {
|
||||
logger.WarnToConsole("Unable to generate man page files: %v", err)
|
||||
|
|
|
@ -21,6 +21,7 @@ type ShareScope int
|
|||
const (
|
||||
ShareScopeRead ShareScope = iota + 1
|
||||
ShareScopeWrite
|
||||
ShareScopeReadWrite
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -64,10 +65,12 @@ type Share struct {
|
|||
// Used in web pages
|
||||
func (s *Share) GetScopeAsString() string {
|
||||
switch s.Scope {
|
||||
case ShareScopeRead:
|
||||
return "Read"
|
||||
default:
|
||||
case ShareScopeWrite:
|
||||
return "Write"
|
||||
case ShareScopeReadWrite:
|
||||
return "Read/Write"
|
||||
default:
|
||||
return "Read"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -194,7 +197,7 @@ func (s *Share) validatePaths() error {
|
|||
s.Paths[idx] = util.CleanPath(s.Paths[idx])
|
||||
}
|
||||
s.Paths = util.RemoveDuplicates(s.Paths)
|
||||
if s.Scope == ShareScopeWrite && len(s.Paths) != 1 {
|
||||
if s.Scope >= ShareScopeWrite && len(s.Paths) != 1 {
|
||||
return util.NewValidationError("the write share scope requires exactly one path")
|
||||
}
|
||||
// check nested paths
|
||||
|
@ -220,7 +223,7 @@ func (s *Share) validate() error {
|
|||
if s.Name == "" {
|
||||
return util.NewValidationError("name is mandatory")
|
||||
}
|
||||
if s.Scope != ShareScopeRead && s.Scope != ShareScopeWrite {
|
||||
if s.Scope < ShareScopeRead || s.Scope > ShareScopeReadWrite {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid scope: %v", s.Scope))
|
||||
}
|
||||
if err := s.validatePaths(); err != nil {
|
||||
|
|
|
@ -6,8 +6,10 @@ import (
|
|||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -3463,7 +3465,7 @@ func getExitCodeScriptContent(exitCode int) []byte {
|
|||
|
||||
func createTestFile(path string, size int64) error {
|
||||
baseDir := filepath.Dir(path)
|
||||
if _, err := os.Stat(baseDir); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) {
|
||||
err = os.MkdirAll(baseDir, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -3,7 +3,9 @@ package ftpd
|
|||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -735,7 +737,7 @@ func TestAVBLErrors(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
_, err = connection.GetAvailableSpace("/missing-path")
|
||||
assert.Error(t, err)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrNotExist))
|
||||
}
|
||||
|
||||
func TestUploadOverwriteErrors(t *testing.T) {
|
||||
|
|
|
@ -290,7 +290,7 @@ func doUploadFiles(w http.ResponseWriter, r *http.Request, connection *Connectio
|
|||
}
|
||||
defer file.Close()
|
||||
|
||||
filePath := path.Join(parentDir, f.Filename)
|
||||
filePath := path.Join(parentDir, path.Base(util.CleanPath(f.Filename)))
|
||||
writer, err := connection.getFileWriter(filePath)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to write file %#v", f.Filename), getMappedStatusCode(err))
|
||||
|
|
|
@ -153,7 +153,8 @@ func deleteShare(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func (s *httpdServer) readBrowsableShareContents(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
|
||||
validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
|
||||
share, connection, err := s.checkPublicShare(w, r, validScopes, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -183,7 +184,8 @@ func (s *httpdServer) readBrowsableShareContents(w http.ResponseWriter, r *http.
|
|||
|
||||
func (s *httpdServer) downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
|
||||
validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
|
||||
share, connection, err := s.checkPublicShare(w, r, validScopes, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -232,7 +234,8 @@ func (s *httpdServer) downloadBrowsableSharedFile(w http.ResponseWriter, r *http
|
|||
|
||||
func (s *httpdServer) downloadFromShare(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
|
||||
validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
|
||||
share, connection, err := s.checkPublicShare(w, r, validScopes, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -264,7 +267,7 @@ func (s *httpdServer) downloadFromShare(w http.ResponseWriter, r *http.Request)
|
|||
connection.Log(logger.LevelInfo, "denying share read due to quota limits")
|
||||
sendAPIResponse(w, r, err, "", getMappedStatusCode(err))
|
||||
}
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"share-%v.zip\"", share.ShareID))
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"share-%v.zip\"", share.Name))
|
||||
renderCompressedFiles(w, connection, "/", share.Paths, &share)
|
||||
return
|
||||
}
|
||||
|
@ -287,12 +290,17 @@ func (s *httpdServer) uploadFileToShare(w http.ResponseWriter, r *http.Request)
|
|||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
|
||||
}
|
||||
name := getURLParam(r, "name")
|
||||
share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, false)
|
||||
validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite}
|
||||
share, connection, err := s.checkPublicShare(w, r, validScopes, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
filePath := path.Join(share.Paths[0], name)
|
||||
if path.Dir(filePath) != share.Paths[0] {
|
||||
filePath := util.CleanPath(path.Join(share.Paths[0], name))
|
||||
expectedPrefix := share.Paths[0]
|
||||
if !strings.HasSuffix(expectedPrefix, "/") {
|
||||
expectedPrefix += "/"
|
||||
}
|
||||
if !strings.HasPrefix(filePath, expectedPrefix) {
|
||||
sendAPIResponse(w, r, err, "Uploading outside the share is not allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
@ -312,7 +320,8 @@ func (s *httpdServer) uploadFilesToShare(w http.ResponseWriter, r *http.Request)
|
|||
if maxUploadFileSize > 0 {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
|
||||
}
|
||||
share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, false)
|
||||
validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite}
|
||||
share, connection, err := s.checkPublicShare(w, r, validScopes, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -361,7 +370,7 @@ func (s *httpdServer) uploadFilesToShare(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
}
|
||||
|
||||
func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, shareShope dataprovider.ShareScope,
|
||||
func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, validScopes []dataprovider.ShareScope,
|
||||
isWebClient bool,
|
||||
) (dataprovider.Share, *Connection, error) {
|
||||
renderError := func(err error, message string, statusCode int) {
|
||||
|
@ -382,7 +391,7 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, s
|
|||
renderError(err, "", statusCode)
|
||||
return share, nil, err
|
||||
}
|
||||
if share.Scope != shareShope {
|
||||
if !util.Contains(validScopes, share.Scope) {
|
||||
renderError(nil, "Invalid share scope", http.StatusForbidden)
|
||||
return share, nil, errors.New("invalid share scope")
|
||||
}
|
||||
|
@ -406,16 +415,11 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, s
|
|||
return share, nil, dataprovider.ErrInvalidCredentials
|
||||
}
|
||||
}
|
||||
user, err := dataprovider.GetUserWithGroupSettings(share.Username)
|
||||
user, err := getUserForShare(share)
|
||||
if err != nil {
|
||||
renderError(err, "", getRespStatus(err))
|
||||
return share, nil, err
|
||||
}
|
||||
if user.MustSetSecondFactorForProtocol(common.ProtocolHTTP) {
|
||||
err := util.NewMethodDisabledError("two-factor authentication requirements not met")
|
||||
renderError(err, "", getRespStatus(err))
|
||||
return share, nil, err
|
||||
}
|
||||
connID := xid.New().String()
|
||||
connection := &Connection{
|
||||
BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTPShare, util.GetHTTPLocalAddress(r),
|
||||
|
@ -426,6 +430,23 @@ func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, s
|
|||
return share, connection, nil
|
||||
}
|
||||
|
||||
func getUserForShare(share dataprovider.Share) (dataprovider.User, error) {
|
||||
user, err := dataprovider.GetUserWithGroupSettings(share.Username)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
if !user.CanManageShares() {
|
||||
return user, util.NewRecordNotFoundError("this share does not exist")
|
||||
}
|
||||
if share.Password == "" && util.Contains(user.Filters.WebClient, sdk.WebClientShareNoPasswordDisabled) {
|
||||
return user, fmt.Errorf("sharing without a password was disabled: %w", os.ErrPermission)
|
||||
}
|
||||
if user.MustSetSecondFactorForProtocol(common.ProtocolHTTP) {
|
||||
return user, util.NewMethodDisabledError("two-factor authentication requirements not met")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func validateBrowsableShare(share dataprovider.Share, connection *Connection) error {
|
||||
if len(share.Paths) != 1 {
|
||||
return util.NewValidationError("a share with multiple paths is not browsable")
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
@ -79,10 +80,10 @@ func getRespStatus(err error) int {
|
|||
if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||
return http.StatusNotFound
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
if os.IsPermission(err) || errors.Is(err, dataprovider.ErrLoginNotAllowedFromIP) {
|
||||
if errors.Is(err, fs.ErrPermission) || errors.Is(err, dataprovider.ErrLoginNotAllowedFromIP) {
|
||||
return http.StatusForbidden
|
||||
}
|
||||
if errors.Is(err, plugin.ErrNoSearcher) || errors.Is(err, dataprovider.ErrNotImplemented) {
|
||||
|
@ -241,7 +242,11 @@ func addZipEntry(wr *zip.Writer, conn *Connection, entryPath, baseDir string) er
|
|||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
_, err := wr.Create(getZipEntryName(entryPath, baseDir) + "/")
|
||||
_, err := wr.CreateHeader(&zip.FileHeader{
|
||||
Name: getZipEntryName(entryPath, baseDir) + "/",
|
||||
Method: zip.Deflate,
|
||||
Modified: info.ModTime(),
|
||||
})
|
||||
if err != nil {
|
||||
conn.Log(logger.LevelDebug, "unable to create zip entry %#v: %v", entryPath, err)
|
||||
return err
|
||||
|
@ -271,7 +276,11 @@ func addZipEntry(wr *zip.Writer, conn *Connection, entryPath, baseDir string) er
|
|||
}
|
||||
defer reader.Close()
|
||||
|
||||
f, err := wr.Create(getZipEntryName(entryPath, baseDir))
|
||||
f, err := wr.CreateHeader(&zip.FileHeader{
|
||||
Name: getZipEntryName(entryPath, baseDir),
|
||||
Method: zip.Deflate,
|
||||
Modified: info.ModTime(),
|
||||
})
|
||||
if err != nil {
|
||||
conn.Log(logger.LevelDebug, "unable to create zip entry %#v: %v", entryPath, err)
|
||||
return err
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
|
@ -8561,7 +8562,7 @@ func TestStartQuotaScanMock(t *testing.T) {
|
|||
waitForUsersQuotaScan(t, token)
|
||||
|
||||
_, err = os.Stat(user.HomeDir)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
if err != nil && errors.Is(err, fs.ErrNotExist) {
|
||||
err = os.MkdirAll(user.HomeDir, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
@ -8725,7 +8726,7 @@ func TestStartFolderQuotaScanMock(t *testing.T) {
|
|||
assert.True(t, common.QuotaScans.RemoveVFolderQuotaScan(folderName))
|
||||
// and now a real quota scan
|
||||
_, err = os.Stat(mappedPath)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
if err != nil && errors.Is(err, fs.ErrNotExist) {
|
||||
err = os.MkdirAll(mappedPath, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
@ -10137,6 +10138,10 @@ func TestShareUsage(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "permission denied")
|
||||
|
||||
user.Permissions["/"] = []string{dataprovider.PermAny}
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
body = new(bytes.Buffer)
|
||||
writer = multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("filename", "file1.txt")
|
||||
|
@ -10155,7 +10160,37 @@ func TestShareUsage(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "No files uploaded!")
|
||||
|
||||
share.Scope = dataprovider.ShareScopeRead
|
||||
user.Filters.WebClient = []string{sdk.WebClientSharesDisabled}
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, sharesPath+"/"+objectID, reader)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
|
||||
user.Filters.WebClient = []string{sdk.WebClientShareNoPasswordDisabled}
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
share.Password = ""
|
||||
err = dataprovider.UpdateShare(&share, user.Username, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, sharesPath+"/"+objectID, reader)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "sharing without a password was disabled")
|
||||
|
||||
user.Filters.WebClient = []string{sdk.WebClientInfoChangeDisabled}
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
share.Scope = dataprovider.ShareScopeReadWrite
|
||||
share.Paths = []string{"/missing"}
|
||||
err = dataprovider.UpdateShare(&share, user.Username, "")
|
||||
assert.NoError(t, err)
|
||||
|
@ -10347,12 +10382,6 @@ func TestShareUploadSingle(t *testing.T) {
|
|||
if assert.NoError(t, err) {
|
||||
assert.InDelta(t, util.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(3000))
|
||||
}
|
||||
// we don't allow to create the file in subdirectories
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, "%2Fdir%2Ffile1.txt"), bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, "dir", "file.dat"), bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
|
@ -10391,6 +10420,76 @@ func TestShareUploadSingle(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
}
|
||||
|
||||
func TestShareReadWrite(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Filters.StartDirectory = path.Join("/start", "dir")
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
|
||||
assert.NoError(t, err)
|
||||
testFileName := "test.txt"
|
||||
|
||||
share := dataprovider.Share{
|
||||
Name: "test share rw",
|
||||
Scope: dataprovider.ShareScopeReadWrite,
|
||||
Paths: []string{user.Filters.StartDirectory},
|
||||
Password: defaultPassword,
|
||||
MaxTokens: 0,
|
||||
}
|
||||
asJSON, err := json.Marshal(share)
|
||||
assert.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
objectID := rr.Header().Get("X-Object-ID")
|
||||
assert.NotEmpty(t, objectID)
|
||||
|
||||
content := []byte("shared rw content")
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, testFileName), bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
assert.FileExists(t, filepath.Join(user.GetHomeDir(), user.Filters.StartDirectory, testFileName))
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "browse?path=%2F"), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "browse?path="+testFileName), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
contentDisposition := rr.Header().Get("Content-Disposition")
|
||||
assert.NotEmpty(t, contentDisposition)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID)+"/"+url.PathEscape("../"+testFileName),
|
||||
bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Uploading outside the share is not allowed")
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID)+"/"+url.PathEscape("/../../"+testFileName),
|
||||
bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Uploading outside the share is not allowed")
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestShareUncompressed(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
@ -10799,7 +10898,7 @@ func TestBrowseShares(t *testing.T) {
|
|||
req, err = http.NewRequest(http.MethodGet, path.Join(sharesPath, objectID, "dirs"), nil)
|
||||
assert.NoError(t, err)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "unable to check the share directory")
|
||||
// share multiple paths
|
||||
share = dataprovider.Share{
|
||||
|
@ -10868,6 +10967,32 @@ func TestBrowseShares(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "two-factor authentication requirements not met")
|
||||
user.Filters.TwoFactorAuthProtocols = []string{common.ProtocolSSH}
|
||||
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
// share read/write
|
||||
share.Scope = dataprovider.ShareScopeReadWrite
|
||||
asJSON, err = json.Marshal(share)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
objectID = rr.Header().Get("X-Object-ID")
|
||||
assert.NotEmpty(t, objectID)
|
||||
req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "browse?path=%2F"), nil)
|
||||
assert.NoError(t, err)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
// on upload we should be redirected
|
||||
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.StatusFound, rr)
|
||||
location := rr.Header().Get("Location")
|
||||
assert.Equal(t, path.Join(webClientPubSharesPath, objectID, "browse"), location)
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -18853,7 +18978,7 @@ func checkResponseCode(t *testing.T, expected int, rr *httptest.ResponseRecorder
|
|||
|
||||
func createTestFile(path string, size int64) error {
|
||||
baseDir := filepath.Dir(path)
|
||||
if _, err := os.Stat(baseDir); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) {
|
||||
err = os.MkdirAll(baseDir, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -2292,6 +2292,12 @@ func TestMetadataAPI(t *testing.T) {
|
|||
func TestBrowsableSharePaths(t *testing.T) {
|
||||
share := dataprovider.Share{
|
||||
Paths: []string{"/"},
|
||||
Username: defaultAdminUsername,
|
||||
}
|
||||
_, err := getUserForShare(share)
|
||||
if assert.Error(t, err) {
|
||||
_, ok := err.(*util.RecordNotFoundError)
|
||||
assert.True(t, ok)
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, "/share", nil)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -147,8 +147,10 @@ type shareFilesPage struct {
|
|||
DirsURL string
|
||||
FilesURL string
|
||||
DownloadURL string
|
||||
UploadBaseURL string
|
||||
Error string
|
||||
Paths []dirMapping
|
||||
Scope dataprovider.ShareScope
|
||||
}
|
||||
|
||||
type shareUploadPage struct {
|
||||
|
@ -512,8 +514,10 @@ func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Reque
|
|||
DirsURL: path.Join(webClientPubSharesPath, share.ShareID, "dirs"),
|
||||
FilesURL: currentURL,
|
||||
DownloadURL: path.Join(webClientPubSharesPath, share.ShareID),
|
||||
UploadBaseURL: path.Join(webClientPubSharesPath, share.ShareID, url.PathEscape(dirName)),
|
||||
Error: error,
|
||||
Paths: getDirMapping(dirName, currentURL),
|
||||
Scope: share.Scope,
|
||||
}
|
||||
renderClientTemplate(w, templateShareFiles, data)
|
||||
}
|
||||
|
@ -625,7 +629,8 @@ func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http.
|
|||
|
||||
func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, true)
|
||||
validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
|
||||
share, connection, err := s.checkPublicShare(w, r, validScopes, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -674,16 +679,22 @@ func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.R
|
|||
|
||||
func (s *httpdServer) handleClientUploadToShare(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, _, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, true)
|
||||
validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeWrite, dataprovider.ShareScopeReadWrite}
|
||||
share, _, err := s.checkPublicShare(w, r, validScopes, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if share.Scope == dataprovider.ShareScopeReadWrite {
|
||||
http.Redirect(w, r, path.Join(webClientPubSharesPath, share.ShareID, "browse"), http.StatusFound)
|
||||
return
|
||||
}
|
||||
s.renderUploadToSharePage(w, r, share)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, true)
|
||||
validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeRead, dataprovider.ShareScopeReadWrite}
|
||||
share, connection, err := s.checkPublicShare(w, r, validScopes, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ package logger
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
@ -57,7 +58,7 @@ func InitLogger(logFilePath string, logMaxSize int, logMaxBackups int, logMaxAge
|
|||
SetLogTime(logUTCTime)
|
||||
if isLogFilePathValid(logFilePath) {
|
||||
logDir := filepath.Dir(logFilePath)
|
||||
if _, err := os.Stat(logDir); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(logDir); errors.Is(err, fs.ErrNotExist) {
|
||||
err = os.MkdirAll(logDir, os.ModePerm)
|
||||
if err != nil {
|
||||
fmt.Printf("unable to create log dir %#v: %v", logDir, err)
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
|
@ -755,7 +756,7 @@ func (c *Configuration) generateDefaultHostKeys(configDir string) error {
|
|||
defaultHostKeys := []string{defaultPrivateRSAKeyName, defaultPrivateECDSAKeyName, defaultPrivateEd25519KeyName}
|
||||
for _, k := range defaultHostKeys {
|
||||
autoFile := filepath.Join(configDir, k)
|
||||
if _, err = os.Stat(autoFile); os.IsNotExist(err) {
|
||||
if _, err = os.Stat(autoFile); errors.Is(err, fs.ErrNotExist) {
|
||||
logger.Info(logSender, "", "No host keys configured and %#v does not exist; try to create a new host key", autoFile)
|
||||
logger.InfoToConsole("No host keys configured and %#v does not exist; try to create a new host key", autoFile)
|
||||
if k == defaultPrivateRSAKeyName {
|
||||
|
@ -780,7 +781,7 @@ func (c *Configuration) generateDefaultHostKeys(configDir string) error {
|
|||
func (c *Configuration) checkHostKeyAutoGeneration(configDir string) error {
|
||||
for _, k := range c.HostKeys {
|
||||
if filepath.IsAbs(k) {
|
||||
if _, err := os.Stat(k); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(k); errors.Is(err, fs.ErrNotExist) {
|
||||
keyName := filepath.Base(k)
|
||||
switch keyName {
|
||||
case defaultPrivateRSAKeyName:
|
||||
|
|
|
@ -10,9 +10,11 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
|
@ -1463,11 +1465,11 @@ func TestStat(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
_, err = client.Stat(testFileName)
|
||||
assert.NoError(t, err)
|
||||
// stat a missing path we should get an os.IsNotExist error
|
||||
// stat a missing path we should get an fs.ErrNotExist error
|
||||
_, err = client.Stat("missing path")
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrNotExist))
|
||||
_, err = client.Lstat("missing path")
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrNotExist))
|
||||
// mode 0666 and 0444 works on Windows too
|
||||
newPerm := os.FileMode(0666)
|
||||
err = client.Chmod(testFileName, newPerm)
|
||||
|
@ -6924,7 +6926,7 @@ func TestOpenError(t *testing.T) {
|
|||
err = os.Chmod(filepath.Join(user.GetHomeDir(), testDir), 0000)
|
||||
assert.NoError(t, err)
|
||||
err = client.Rename(testFileName, path.Join(testDir, testFileName))
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = os.Chmod(filepath.Join(user.GetHomeDir(), testDir), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(localDownloadPath)
|
||||
|
@ -7253,7 +7255,7 @@ func TestPermRename(t *testing.T) {
|
|||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = client.Rename(testFileName, testFileName+".rename")
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
_, err = client.Stat(testFileName)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(testFilePath)
|
||||
|
@ -7289,7 +7291,7 @@ func TestPermRenameOverwrite(t *testing.T) {
|
|||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = client.Rename(testFileName, testFileName+".rename")
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Remove(testFileName)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(testFilePath)
|
||||
|
@ -7474,13 +7476,13 @@ func TestSubDirsUploads(t *testing.T) {
|
|||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, testFileNameSub, testFileSize, client)
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Symlink(testFileName, testFileNameSub+".link")
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Symlink(testFileName, testFileName+".link")
|
||||
assert.NoError(t, err)
|
||||
err = client.Rename(testFileName, testFileNameSub+".rename")
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Rename(testFileName, testFileName+".rename")
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
|
@ -7500,7 +7502,7 @@ func TestSubDirsUploads(t *testing.T) {
|
|||
err = client.Remove(testDir)
|
||||
assert.NoError(t, err)
|
||||
err = client.Remove(path.Join("/subdir", "file.dat"))
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Remove(testFileName + ".rename")
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(testFilePath)
|
||||
|
@ -7532,7 +7534,7 @@ func TestSubDirsOverwrite(t *testing.T) {
|
|||
err = createTestFile(testFileSFTPPath, 16384)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, testFileName+".new", testFileSize, client)
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(testFilePath)
|
||||
|
@ -7566,17 +7568,17 @@ func TestSubDirsDownloads(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Chtimes(testFileName, time.Now(), time.Now())
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Rename(testFileName, testFileName+".rename")
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Symlink(testFileName, testFileName+".link")
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Remove(testFileName)
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = os.Remove(localDownloadPath)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(testFilePath)
|
||||
|
@ -7611,9 +7613,9 @@ func TestPermsSubDirsSetstat(t *testing.T) {
|
|||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = client.Chtimes("/subdir/", time.Now(), time.Now())
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Chtimes("subdir/", time.Now(), time.Now())
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Chtimes(testFileName, time.Now(), time.Now())
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(testFilePath)
|
||||
|
@ -7674,19 +7676,19 @@ func TestPermsSubDirsCommands(t *testing.T) {
|
|||
_, err = client.ReadDir("/")
|
||||
assert.NoError(t, err)
|
||||
_, err = client.ReadDir("/subdir")
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.RemoveDirectory("/subdir/dir")
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Mkdir("/subdir/otherdir/dir")
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Mkdir("/otherdir")
|
||||
assert.NoError(t, err)
|
||||
err = client.Mkdir("/subdir/otherdir")
|
||||
assert.NoError(t, err)
|
||||
err = client.Rename("/otherdir", "/subdir/otherdir/adir")
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Symlink("/otherdir", "/subdir/otherdir")
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Symlink("/otherdir", "/otherdir_link")
|
||||
assert.NoError(t, err)
|
||||
err = client.Rename("/otherdir", "/otherdir1")
|
||||
|
@ -7718,11 +7720,11 @@ func TestRootDirCommands(t *testing.T) {
|
|||
defer conn.Close()
|
||||
defer client.Close()
|
||||
err = client.Rename("/", "rootdir")
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.Symlink("/", "rootdir")
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
err = client.RemoveDirectory("/")
|
||||
assert.True(t, os.IsPermission(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrPermission))
|
||||
}
|
||||
if user.Username == defaultUsername {
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
|
@ -8200,7 +8202,7 @@ func TestStatVFS(t *testing.T) {
|
|||
|
||||
_, err = client.StatVFS("missing-path")
|
||||
assert.Error(t, err)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
assert.True(t, errors.Is(err, fs.ErrNotExist))
|
||||
}
|
||||
user.QuotaFiles = 100
|
||||
user.Filters.DisableFsChecks = true
|
||||
|
@ -10524,7 +10526,7 @@ func getCustomAuthSftpClient(user dataprovider.User, authMethods []ssh.AuthMetho
|
|||
|
||||
func createTestFile(path string, size int64) error {
|
||||
baseDir := filepath.Dir(path)
|
||||
if _, err := os.Stat(baseDir); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) {
|
||||
err = os.MkdirAll(baseDir, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
{{define "title"}}{{.Title}}{{end}}
|
||||
|
||||
{{define "extra_css"}}
|
||||
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
|
||||
{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
|
@ -26,7 +30,7 @@
|
|||
<div class="form-group row">
|
||||
<label for="idConfig" class="col-sm-2 col-form-label">Configuration</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idConfig" name="config_name">
|
||||
<select class="form-control selectpicker" id="idConfig" name="config_name">
|
||||
<option value="">None</option>
|
||||
{{range .TOTPConfigs}}
|
||||
<option value="{{.}}" {{if eq . $.TOTPConfig.ConfigName}}selected{{end}}>{{.}}</option>
|
||||
|
@ -127,6 +131,7 @@
|
|||
{{end}}
|
||||
|
||||
{{define "extra_js"}}
|
||||
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
function totpGenerate() {
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
{{define "title"}}{{.Title}}{{end}}
|
||||
|
||||
{{define "extra_css"}}
|
||||
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
|
||||
{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
|
@ -33,7 +37,7 @@
|
|||
<div class="form-group row totpProtocols">
|
||||
<label for="idProtocols" class="col-sm-3 col-form-label">Require two-factor auth for</label>
|
||||
<div class="col-sm-9">
|
||||
<select class="form-control" id="idProtocols" name="multi_factor_protocols" multiple>
|
||||
<select class="form-control selectpicker" id="idProtocols" name="multi_factor_protocols" multiple>
|
||||
{{range $protocol := .Protocols}}
|
||||
<option value="{{$protocol}}" {{range $p :=$.TOTPConfig.Protocols }}{{if eq $p $protocol}}selected{{end}}{{end}}>{{$protocol}}
|
||||
</option>
|
||||
|
@ -51,7 +55,7 @@
|
|||
<div class="form-group row">
|
||||
<label for="idConfig" class="col-sm-3 col-form-label">Configuration</label>
|
||||
<div class="col-sm-9">
|
||||
<select class="form-control" id="idConfig" name="config_name">
|
||||
<select class="form-control selectpicker" id="idConfig" name="config_name">
|
||||
<option value="">None</option>
|
||||
{{range .TOTPConfigs}}
|
||||
<option value="{{.}}" {{if eq . $.TOTPConfig.ConfigName}}selected{{end}}>{{.}}</option>
|
||||
|
@ -152,6 +156,7 @@
|
|||
{{end}}
|
||||
|
||||
{{define "extra_js"}}
|
||||
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
function totpGenerate() {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
{{define "extra_css"}}
|
||||
<link href="{{.StaticURL}}/vendor/tempusdominus/css/tempusdominus-bootstrap-4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
|
||||
{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
|
@ -29,12 +30,13 @@
|
|||
<div class="form-group row">
|
||||
<label for="idScope" class="col-sm-2 col-form-label">Scope</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="idScope" name="scope" aria-describedby="scopeHelpBlock">
|
||||
<select class="form-control selectpicker" id="idScope" name="scope" aria-describedby="scopeHelpBlock">
|
||||
<option value="1" {{if eq .Share.Scope 1 }}selected{{end}}>Read</option>
|
||||
<option value="2" {{if eq .Share.Scope 2 }}selected{{end}}>Write</option>
|
||||
<option value="3" {{if eq .Share.Scope 3 }}selected{{end}}>Read/Write</option>
|
||||
</select>
|
||||
<small id="scopeHelpBlock" class="form-text text-muted">
|
||||
For scope "Write" you have to define one path and it must be a directory
|
||||
For scope "Write" and "Read&Write" you have to define one path and it must be a directory
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -144,6 +146,7 @@
|
|||
{{define "extra_js"}}
|
||||
<script src="{{.StaticURL}}/vendor/moment/js/moment.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/tempusdominus/js/tempusdominus-bootstrap-4.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function () {
|
||||
|
||||
|
|
|
@ -38,6 +38,37 @@
|
|||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "dialog"}}
|
||||
<div class="modal fade" id="uploadFilesModal" tabindex="-1" role="dialog" aria-labelledby="uploadFilesModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="uploadFilesModalLabel">
|
||||
Upload one or more files
|
||||
</h5>
|
||||
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form id="upload_files_form" action="{{.FilesURL}}?path={{.CurrentDir}}" 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>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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 src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
|
||||
|
@ -171,6 +202,94 @@
|
|||
}
|
||||
|
||||
$(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;
|
||||
|
||||
$('#uploadFilesModal').modal('hide');
|
||||
$('#spinnerModal').modal('show');
|
||||
|
||||
function uploadFile() {
|
||||
if (index >= files.length || has_errors){
|
||||
$('#spinnerModal').modal('hide');
|
||||
spinnerDone = true;
|
||||
if (!has_errors){
|
||||
location.reload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async function saveFile() {
|
||||
var errorMessage = "Error uploading files";
|
||||
let response;
|
||||
try {
|
||||
var f = files[index];
|
||||
var uploadPath = '{{.UploadBaseURL}}'+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();
|
||||
});
|
||||
|
||||
$.fn.dataTable.ext.buttons.refresh = {
|
||||
text: '<i class="fas fa-sync-alt"></i>',
|
||||
name: 'refresh',
|
||||
|
@ -191,6 +310,17 @@
|
|||
}
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.addFiles = {
|
||||
text: '<i class="fas fa-file-upload"></i>',
|
||||
name: 'addFiles',
|
||||
titleAttr: "Upload files",
|
||||
action: function (e, dt, node, config) {
|
||||
document.getElementById("files_name").value = null;
|
||||
$('#uploadFilesModal').modal('show');
|
||||
},
|
||||
enabled: true
|
||||
};
|
||||
|
||||
var table = $('#dataTable').DataTable({
|
||||
"ajax": {
|
||||
"url": "{{.DirsURL}}?path={{.CurrentDir}}",
|
||||
|
@ -283,6 +413,9 @@
|
|||
table.button().add(0, 'refresh');
|
||||
table.button().add(0, 'pageLength');
|
||||
table.button().add(0, 'download');
|
||||
{{if gt .Scope 1}}
|
||||
table.button().add(0, 'addFiles');
|
||||
{{end}}
|
||||
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container());
|
||||
},
|
||||
"orderFixed": [0, 'asc'],
|
||||
|
|
14
util/util.go
14
util/util.go
|
@ -15,6 +15,7 @@ import (
|
|||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
@ -45,7 +46,18 @@ var (
|
|||
trueClientIP = http.CanonicalHeaderKey("True-Client-IP")
|
||||
)
|
||||
|
||||
// Contains reports whether v is present in elems.
|
||||
func Contains[T comparable](elems []T, v T) bool {
|
||||
for _, s := range elems {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsStringInSlice searches a string in a slice and returns true if the string is found
|
||||
// TODO: replace with Contains above
|
||||
func IsStringInSlice(obj string, list []string) bool {
|
||||
for i := 0; i < len(list); i++ {
|
||||
if list[i] == obj {
|
||||
|
@ -390,7 +402,7 @@ func CleanDirInput(dirInput string) string {
|
|||
|
||||
func createDirPathIfMissing(file string, perm os.FileMode) error {
|
||||
dirPath := filepath.Dir(file)
|
||||
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(dirPath); errors.Is(err, fs.ErrNotExist) {
|
||||
err = os.MkdirAll(dirPath, perm)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
14
vfs/osfs.go
14
vfs/osfs.go
|
@ -1,8 +1,10 @@
|
|||
package vfs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
@ -200,7 +202,7 @@ func (*OsFs) IsAtomicUploadSupported() bool {
|
|||
// IsNotExist returns a boolean indicating whether the error is known to
|
||||
// report that a file or directory does not exist
|
||||
func (*OsFs) IsNotExist(err error) bool {
|
||||
return os.IsNotExist(err)
|
||||
return errors.Is(err, fs.ErrNotExist)
|
||||
}
|
||||
|
||||
// IsPermission returns a boolean indicating whether the error is known to
|
||||
|
@ -209,8 +211,7 @@ func (*OsFs) IsPermission(err error) bool {
|
|||
if _, ok := err.(*pathResolutionError); ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return os.IsPermission(err)
|
||||
return errors.Is(err, fs.ErrPermission)
|
||||
}
|
||||
|
||||
// IsNotSupported returns true if the error indicate an unsupported operation
|
||||
|
@ -297,9 +298,10 @@ func (fs *OsFs) ResolvePath(virtualPath string) (string, error) {
|
|||
if isInvalidNameError(err) {
|
||||
err = os.ErrNotExist
|
||||
}
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
isNotExist := fs.IsNotExist(err)
|
||||
if err != nil && !isNotExist {
|
||||
return "", err
|
||||
} else if os.IsNotExist(err) {
|
||||
} else if isNotExist {
|
||||
// The requested path doesn't exist, so at this point we need to iterate up the
|
||||
// path chain until we hit a directory that _does_ exist and can be validated.
|
||||
_, err = fs.findFirstExistingDir(r)
|
||||
|
@ -349,7 +351,7 @@ func (fs *OsFs) findNonexistentDirs(filePath string) ([]string, error) {
|
|||
parent := filepath.Dir(cleanPath)
|
||||
_, err := os.Stat(parent)
|
||||
|
||||
for os.IsNotExist(err) {
|
||||
for fs.IsNotExist(err) {
|
||||
results = append(results, parent)
|
||||
parent = filepath.Dir(parent)
|
||||
_, err = os.Stat(parent)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -431,7 +432,7 @@ func (fs *SFTPFs) IsAtomicUploadSupported() bool {
|
|||
// IsNotExist returns a boolean indicating whether the error is known to
|
||||
// report that a file or directory does not exist
|
||||
func (*SFTPFs) IsNotExist(err error) bool {
|
||||
return os.IsNotExist(err)
|
||||
return errors.Is(err, fs.ErrNotExist)
|
||||
}
|
||||
|
||||
// IsPermission returns a boolean indicating whether the error is known to
|
||||
|
@ -440,7 +441,7 @@ func (*SFTPFs) IsPermission(err error) bool {
|
|||
if _, ok := err.(*pathResolutionError); ok {
|
||||
return true
|
||||
}
|
||||
return os.IsPermission(err)
|
||||
return errors.Is(err, fs.ErrPermission)
|
||||
}
|
||||
|
||||
// IsNotSupported returns true if the error indicate an unsupported operation
|
||||
|
@ -559,12 +560,13 @@ func (fs *SFTPFs) ResolvePath(virtualPath string) (string, error) {
|
|||
var validatedPath string
|
||||
var err error
|
||||
validatedPath, err = fs.getRealPath(fsPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
isNotExist := fs.IsNotExist(err)
|
||||
if err != nil && !isNotExist {
|
||||
fsLog(fs, logger.LevelError, "Invalid path resolution, original path %v resolved %#v err: %v",
|
||||
virtualPath, fsPath, err)
|
||||
return "", err
|
||||
} else if os.IsNotExist(err) {
|
||||
for os.IsNotExist(err) {
|
||||
} else if isNotExist {
|
||||
for fs.IsNotExist(err) {
|
||||
validatedPath = path.Dir(validatedPath)
|
||||
if validatedPath == "/" {
|
||||
err = nil
|
||||
|
|
|
@ -644,13 +644,13 @@ func TestRemoveDirTree(t *testing.T) {
|
|||
p := filepath.Join(user.HomeDir, "adir", "missing")
|
||||
err := connection.removeDirTree(fs, p, vpath)
|
||||
if assert.Error(t, err) {
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
assert.True(t, fs.IsNotExist(err))
|
||||
}
|
||||
|
||||
fs = newMockOsFs(nil, false, "mockID", user.HomeDir, nil)
|
||||
err = connection.removeDirTree(fs, p, vpath)
|
||||
if assert.Error(t, err) {
|
||||
assert.True(t, os.IsNotExist(err), "unexpected error: %v", err)
|
||||
assert.True(t, fs.IsNotExist(err), "unexpected error: %v", err)
|
||||
}
|
||||
|
||||
errFake := errors.New("fake err")
|
||||
|
@ -663,7 +663,7 @@ func TestRemoveDirTree(t *testing.T) {
|
|||
fs = newMockOsFs(errWalkDir, true, "mockID", user.HomeDir, nil)
|
||||
err = connection.removeDirTree(fs, p, vpath)
|
||||
if assert.Error(t, err) {
|
||||
assert.True(t, os.IsPermission(err), "unexpected error: %v", err)
|
||||
assert.True(t, fs.IsPermission(err), "unexpected error: %v", err)
|
||||
}
|
||||
|
||||
fs = newMockOsFs(errWalkFile, false, "mockID", user.HomeDir, nil)
|
||||
|
@ -766,9 +766,9 @@ func TestTransferReadWriteErrors(t *testing.T) {
|
|||
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})
|
||||
davFile = newWebDavFile(baseTransfer, nil, nil)
|
||||
_, err = davFile.Read(p)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
assert.True(t, fs.IsNotExist(err))
|
||||
_, err = davFile.Stat()
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
assert.True(t, fs.IsNotExist(err))
|
||||
|
||||
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,
|
||||
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})
|
||||
|
@ -852,7 +852,7 @@ func TestTransferSeek(t *testing.T) {
|
|||
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})
|
||||
davFile = newWebDavFile(baseTransfer, nil, nil)
|
||||
_, err = davFile.Seek(0, io.SeekCurrent)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
assert.True(t, fs.IsNotExist(err))
|
||||
davFile.Connection.RemoveTransfer(davFile.BaseTransfer)
|
||||
|
||||
err = os.WriteFile(testFilePath, testFileContents, os.ModePerm)
|
||||
|
@ -888,7 +888,7 @@ func TestTransferSeek(t *testing.T) {
|
|||
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{AllowedTotalSize: 100})
|
||||
davFile = newWebDavFile(baseTransfer, nil, nil)
|
||||
_, err = davFile.Seek(0, io.SeekEnd)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
assert.True(t, fs.IsNotExist(err))
|
||||
davFile.Connection.RemoveTransfer(davFile.BaseTransfer)
|
||||
|
||||
baseTransfer = common.NewBaseTransfer(nil, connection.BaseConnection, nil, testFilePath, testFilePath, testFile,
|
||||
|
@ -912,7 +912,7 @@ func TestTransferSeek(t *testing.T) {
|
|||
davFile = newWebDavFile(baseTransfer, nil, nil)
|
||||
davFile.Fs = newMockOsFs(nil, true, fs.ConnectionID(), user.GetHomeDir(), nil)
|
||||
res, err = davFile.Seek(2, io.SeekEnd)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
assert.True(t, fs.IsNotExist(err))
|
||||
assert.Equal(t, int64(0), res)
|
||||
|
||||
assert.Len(t, common.Connections.GetStats(), 0)
|
||||
|
|
|
@ -6,8 +6,10 @@ import (
|
|||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -2916,7 +2918,7 @@ func getExitCodeScriptContent(exitCode int) []byte {
|
|||
|
||||
func createTestFile(path string, size int64) error {
|
||||
baseDir := filepath.Dir(path)
|
||||
if _, err := os.Stat(baseDir); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(baseDir); errors.Is(err, fs.ErrNotExist) {
|
||||
err = os.MkdirAll(baseDir, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
Loading…
Reference in a new issue