Browse Source

web client: add HTML5 player

See #914

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 3 years ago
parent
commit
a0bbcf6ebb

+ 1 - 1
dataprovider/dataprovider.go

@@ -2091,7 +2091,7 @@ func buildUserHomeDir(user *User) {
 			return
 		}
 		switch user.FsConfig.Provider {
-		case sdk.SFTPFilesystemProvider, sdk.S3FilesystemProvider, sdk.AzureBlobFilesystemProvider, sdk.GCSFilesystemProvider:
+		case sdk.SFTPFilesystemProvider, sdk.S3FilesystemProvider, sdk.AzureBlobFilesystemProvider, sdk.GCSFilesystemProvider, sdk.HTTPFilesystemProvider:
 			if tempPath != "" {
 				user.HomeDir = filepath.Join(tempPath, user.Username)
 			} else {

+ 12 - 2
dataprovider/user.go

@@ -211,6 +211,16 @@ func (u *User) checkDirWithParents(virtualDirPath, connectionID string) error {
 	return nil
 }
 
+func (u *User) checkLocalHomeDir(connectionID string) {
+	switch u.FsConfig.Provider {
+	case sdk.LocalFilesystemProvider, sdk.CryptedFilesystemProvider:
+		return
+	default:
+		osFs := vfs.NewOsFs(connectionID, u.GetHomeDir(), "")
+		osFs.CheckRootPath(u.Username, u.GetUID(), u.GetGID())
+	}
+}
+
 func (u *User) checkRootPath(connectionID string) error {
 	fs, err := u.GetFilesystemForPath("/", connectionID)
 	if err != nil {
@@ -236,8 +246,8 @@ func (u *User) CheckFsRoot(connectionID string) error {
 	}
 	if isLastActivityRecent(u.LastLogin, delay) {
 		if u.LastLogin > u.UpdatedAt {
-			if u.FsConfig.Provider != sdk.LocalFilesystemProvider && config.IsShared == 1 {
-				return u.checkRootPath(connectionID)
+			if config.IsShared == 1 {
+				u.checkLocalHomeDir(connectionID)
 			}
 			return nil
 		}

+ 4 - 5
go.mod

@@ -3,7 +3,7 @@ module github.com/drakkan/sftpgo/v2
 go 1.18
 
 require (
-	cloud.google.com/go/storage v1.23.0
+	cloud.google.com/go/storage v1.24.0
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.1
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1
 	github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
@@ -35,7 +35,7 @@ require (
 	github.com/hashicorp/go-plugin v1.4.4
 	github.com/hashicorp/go-retryablehttp v0.7.1
 	github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
-	github.com/klauspost/compress v1.15.8
+	github.com/klauspost/compress v1.15.9
 	github.com/lestrrat-go/jwx v1.2.25
 	github.com/lib/pq v1.10.6
 	github.com/lithammer/shortuuid/v3 v3.0.7
@@ -70,7 +70,7 @@ require (
 	golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92
 	golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8
 	golang.org/x/time v0.0.0-20220609170525-579cf78fd858
-	google.golang.org/api v0.87.0
+	google.golang.org/api v0.88.0
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
 )
 
@@ -109,7 +109,6 @@ require (
 	github.com/google/go-cmp v0.5.8 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect
 	github.com/googleapis/gax-go/v2 v2.4.0 // indirect
-	github.com/googleapis/go-type-adapters v1.0.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/hashicorp/yamux v0.1.0 // indirect
@@ -155,7 +154,7 @@ require (
 	golang.org/x/tools v0.1.11 // indirect
 	golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
 	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto v0.0.0-20220718134204-073382fd740c // indirect
+	google.golang.org/genproto v0.0.0-20220720214146-176da50484ac // indirect
 	google.golang.org/grpc v1.48.0 // indirect
 	google.golang.org/protobuf v1.28.0 // indirect
 	gopkg.in/ini.v1 v1.66.6 // indirect

+ 8 - 8
go.sum

@@ -75,8 +75,9 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
 cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA=
 cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
-cloud.google.com/go/storage v1.23.0 h1:wWRIaDURQA8xxHguFCshYepGlrWIrbBnAmc7wfg07qY=
 cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
+cloud.google.com/go/storage v1.24.0 h1:a4N0gIkx83uoVFGz8B2eAV3OhN90QoWF5OZWLKl39ig=
+cloud.google.com/go/storage v1.24.0/go.mod h1:3xrJEFMXBsQLgxwThyjuD3aYlroL0TMRec1ypGUQ0KE=
 cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A=
 cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM=
 contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA=
@@ -446,7 +447,6 @@ github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/Oth
 github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
 github.com/googleapis/gax-go/v2 v2.4.0 h1:dS9eYAjhrE2RjmzYw2XAPvcXfmcQLtFEQWn0CR82awk=
 github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
-github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=
 github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
 github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
 github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@@ -542,8 +542,8 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
-github.com/klauspost/compress v1.15.8 h1:JahtItbkWjf2jzm/T+qgMxkP9EMHsqEUA6vCMGmXvhA=
-github.com/klauspost/compress v1.15.8/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
+github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
+github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
 github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
 github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
@@ -1114,8 +1114,8 @@ google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3
 google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
 google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=
 google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
-google.golang.org/api v0.87.0 h1:pUQVF/F+X7Tl1lo4LJoJf5BOpjtmINU80p9XpYTU2p4=
-google.golang.org/api v0.87.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
+google.golang.org/api v0.88.0 h1:MPwxQRqpyskYhr2iNyfsQ8R06eeyhe7UEuR30p136ZQ=
+google.golang.org/api v0.88.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1222,8 +1222,8 @@ google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljW
 google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
-google.golang.org/genproto v0.0.0-20220718134204-073382fd740c h1:xDUAhRezFnKF6wopxkOfdWYvz2XCiRQzndyDdpwFgbc=
-google.golang.org/genproto v0.0.0-20220718134204-073382fd740c/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=
+google.golang.org/genproto v0.0.0-20220720214146-176da50484ac h1:EOa+Yrhx1C0O+4pHeXeWrCwdI0tWI6IfUU56Vebs9wQ=
+google.golang.org/genproto v0.0.0-20220720214146-176da50484ac/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=

+ 2 - 2
httpd/api_utils.go

@@ -443,7 +443,7 @@ func checkIfRange(r *http.Request, modtime time.Time) condResult {
 	if err != nil {
 		return condFalse
 	}
-	if modtime.Add(60 * time.Second).Before(t) {
+	if modtime.Unix() == t.Unix() {
 		return condTrue
 	}
 	return condFalse
@@ -482,7 +482,7 @@ func parseRangeRequest(bytesRange string, size int64) (int64, int64, error) {
 			// we have something like -500
 			start = size - end
 			size = end
-			// start cannit be < 0 here, we did end = size -1 above
+			// start cannot be < 0 here, we did end = size -1 above
 		} else {
 			// we have something like 500-600
 			size = end - start + 1

+ 60 - 53
httpd/httpd_test.go

@@ -12651,6 +12651,8 @@ func TestWebGetFiles(t *testing.T) {
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusPartialContent, rr)
 	assert.Equal(t, testFileContents[2:], rr.Body.Bytes())
+	lastModified, err := http.ParseTime(rr.Header().Get("Last-Modified"))
+	assert.NoError(t, err)
 
 	req, _ = http.NewRequest(http.MethodGet, userFilesPath+"?path="+testFileName, nil)
 	req.Header.Set("Range", "bytes=2-")
@@ -12686,44 +12688,44 @@ func TestWebGetFiles(t *testing.T) {
 
 	req, _ = http.NewRequest(http.MethodHead, webClientFilesPath+"?path="+testFileName, nil)
 	req.Header.Set("Range", "bytes=2-")
-	req.Header.Set("If-Range", time.Now().UTC().Add(120*time.Second).Format(http.TimeFormat))
+	req.Header.Set("If-Range", lastModified.UTC().Format(http.TimeFormat))
 	setJWTCookieForReq(req, webToken)
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusPartialContent, rr)
 
 	req, _ = http.NewRequest(http.MethodHead, webClientFilesPath+"?path="+testFileName, nil)
 	req.Header.Set("Range", "bytes=2-")
-	req.Header.Set("If-Range", time.Now().UTC().Add(-120*time.Second).Format(http.TimeFormat))
+	req.Header.Set("If-Range", lastModified.UTC().Add(-120*time.Second).Format(http.TimeFormat))
 	setJWTCookieForReq(req, webToken)
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusOK, rr)
 
 	req, _ = http.NewRequest(http.MethodHead, webClientFilesPath+"?path="+testFileName, nil)
-	req.Header.Set("If-Modified-Since", time.Now().UTC().Add(-120*time.Second).Format(http.TimeFormat))
+	req.Header.Set("If-Modified-Since", lastModified.UTC().Add(-120*time.Second).Format(http.TimeFormat))
 	setJWTCookieForReq(req, webToken)
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusOK, rr)
 
 	req, _ = http.NewRequest(http.MethodHead, webClientFilesPath+"?path="+testFileName, nil)
-	req.Header.Set("If-Modified-Since", time.Now().UTC().Add(120*time.Second).Format(http.TimeFormat))
+	req.Header.Set("If-Modified-Since", lastModified.UTC().Add(120*time.Second).Format(http.TimeFormat))
 	setJWTCookieForReq(req, webToken)
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusNotModified, rr)
 
 	req, _ = http.NewRequest(http.MethodHead, webClientFilesPath+"?path="+testFileName, nil)
-	req.Header.Set("If-Unmodified-Since", time.Now().UTC().Add(-120*time.Second).Format(http.TimeFormat))
+	req.Header.Set("If-Unmodified-Since", lastModified.UTC().Add(-120*time.Second).Format(http.TimeFormat))
 	setJWTCookieForReq(req, webToken)
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusPreconditionFailed, rr)
 
 	req, _ = http.NewRequest(http.MethodHead, userFilesPath+"?path="+testFileName, nil)
-	req.Header.Set("If-Unmodified-Since", time.Now().UTC().Add(-120*time.Second).Format(http.TimeFormat))
+	req.Header.Set("If-Unmodified-Since", lastModified.UTC().Add(-120*time.Second).Format(http.TimeFormat))
 	setBearerForReq(req, webAPIToken)
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusPreconditionFailed, rr)
 
 	req, _ = http.NewRequest(http.MethodHead, webClientFilesPath+"?path="+testFileName, nil)
-	req.Header.Set("If-Unmodified-Since", time.Now().UTC().Add(120*time.Second).Format(http.TimeFormat))
+	req.Header.Set("If-Unmodified-Since", lastModified.UTC().Add(120*time.Second).Format(http.TimeFormat))
 	setJWTCookieForReq(req, webToken)
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusOK, rr)
@@ -14115,6 +14117,7 @@ func TestGetFilesSFTPBackend(t *testing.T) {
 	user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
 	assert.NoError(t, err)
 	u := getTestSFTPUser()
+	u.HomeDir = filepath.Clean(os.TempDir())
 	u.FsConfig.SFTPConfig.BufferSize = 2
 	u.Permissions["/adir"] = nil
 	u.Permissions["/adir1"] = []string{dataprovider.PermListItems}
@@ -14124,7 +14127,11 @@ func TestGetFilesSFTPBackend(t *testing.T) {
 			DeniedPatterns: []string{"*.txt"},
 		},
 	}
-	sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
+	sftpUserBuffered, _, err := httpdtest.AddUser(u, http.StatusCreated)
+	assert.NoError(t, err)
+	u.Username += "_unbuffered"
+	u.FsConfig.SFTPConfig.BufferSize = 0
+	sftpUserUnbuffered, _, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err)
 
 	testFileName := "testsftpfile"
@@ -14142,58 +14149,58 @@ func TestGetFilesSFTPBackend(t *testing.T) {
 	assert.NoError(t, err)
 	err = os.WriteFile(filepath.Join(user.GetHomeDir(), "adir2", "afile.txt"), testFileContents, os.ModePerm)
 	assert.NoError(t, err)
-	webToken, err := getJWTWebClientTokenFromTestServer(sftpUser.Username, defaultPassword)
-	assert.NoError(t, err)
-	req, _ := http.NewRequest(http.MethodGet, webClientFilesPath, nil)
-	setJWTCookieForReq(req, webToken)
-	rr := executeRequest(req)
-	checkResponseCode(t, http.StatusOK, rr)
+	for _, sftpUser := range []dataprovider.User{sftpUserBuffered, sftpUserUnbuffered} {
+		webToken, err := getJWTWebClientTokenFromTestServer(sftpUser.Username, defaultPassword)
+		assert.NoError(t, err)
+		req, _ := http.NewRequest(http.MethodGet, webClientFilesPath, nil)
+		setJWTCookieForReq(req, webToken)
+		rr := executeRequest(req)
+		checkResponseCode(t, http.StatusOK, rr)
 
-	req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+path.Join(testDir, "sub"), nil)
-	setJWTCookieForReq(req, webToken)
-	rr = executeRequest(req)
-	checkResponseCode(t, http.StatusOK, rr)
+		req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+path.Join(testDir, "sub"), nil)
+		setJWTCookieForReq(req, webToken)
+		rr = executeRequest(req)
+		checkResponseCode(t, http.StatusOK, rr)
 
-	req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+path.Join(testDir, "missing"), nil)
-	setJWTCookieForReq(req, webToken)
-	rr = executeRequest(req)
-	checkResponseCode(t, http.StatusOK, rr)
-	assert.Contains(t, rr.Body.String(), "card-body text-form-error")
-	req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir/sub", nil)
-	setJWTCookieForReq(req, webToken)
-	rr = executeRequest(req)
-	checkResponseCode(t, http.StatusOK, rr)
-	assert.Contains(t, rr.Body.String(), "card-body text-form-error")
+		req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+path.Join(testDir, "missing"), nil)
+		setJWTCookieForReq(req, webToken)
+		rr = executeRequest(req)
+		checkResponseCode(t, http.StatusOK, rr)
+		assert.Contains(t, rr.Body.String(), "card-body text-form-error")
+		req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir/sub", nil)
+		setJWTCookieForReq(req, webToken)
+		rr = executeRequest(req)
+		checkResponseCode(t, http.StatusOK, rr)
+		assert.Contains(t, rr.Body.String(), "card-body text-form-error")
 
-	req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir1/afile", nil)
-	setJWTCookieForReq(req, webToken)
-	rr = executeRequest(req)
-	checkResponseCode(t, http.StatusOK, rr)
-	assert.Contains(t, rr.Body.String(), "card-body text-form-error")
+		req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir1/afile", nil)
+		setJWTCookieForReq(req, webToken)
+		rr = executeRequest(req)
+		checkResponseCode(t, http.StatusOK, rr)
+		assert.Contains(t, rr.Body.String(), "card-body text-form-error")
 
-	req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir2/afile.txt", nil)
-	setJWTCookieForReq(req, webToken)
-	rr = executeRequest(req)
-	checkResponseCode(t, http.StatusOK, rr)
-	assert.Contains(t, rr.Body.String(), "card-body text-form-error")
+		req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path=adir2/afile.txt", nil)
+		setJWTCookieForReq(req, webToken)
+		rr = executeRequest(req)
+		checkResponseCode(t, http.StatusOK, rr)
+		assert.Contains(t, rr.Body.String(), "card-body text-form-error")
 
-	req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+testFileName, nil)
-	setJWTCookieForReq(req, webToken)
-	rr = executeRequest(req)
-	checkResponseCode(t, http.StatusOK, rr)
-	assert.Equal(t, testFileContents, rr.Body.Bytes())
+		req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+testFileName, nil)
+		setJWTCookieForReq(req, webToken)
+		rr = executeRequest(req)
+		checkResponseCode(t, http.StatusOK, rr)
+		assert.Equal(t, testFileContents, rr.Body.Bytes())
 
-	req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+testFileName, nil)
-	req.Header.Set("Range", "bytes=2-")
-	setJWTCookieForReq(req, webToken)
-	rr = executeRequest(req)
-	checkResponseCode(t, http.StatusPartialContent, rr)
-	assert.Equal(t, testFileContents[2:], rr.Body.Bytes())
+		req, _ = http.NewRequest(http.MethodGet, webClientFilesPath+"?path="+testFileName, nil)
+		req.Header.Set("Range", "bytes=2-")
+		setJWTCookieForReq(req, webToken)
+		rr = executeRequest(req)
+		checkResponseCode(t, http.StatusPartialContent, rr)
+		assert.Equal(t, testFileContents[2:], rr.Body.Bytes())
 
-	_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
-	assert.NoError(t, err)
-	err = os.RemoveAll(sftpUser.GetHomeDir())
-	assert.NoError(t, err)
+		_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
+		assert.NoError(t, err)
+	}
 	_, err = httpdtest.RemoveUser(user, http.StatusOK)
 	assert.NoError(t, err)
 	err = os.RemoveAll(user.GetHomeDir())

+ 2 - 0
httpd/internal_test.go

@@ -1043,6 +1043,8 @@ func TestCreateTokenError(t *testing.T) {
 
 	err = dataprovider.DeleteUser(username, "", "")
 	assert.NoError(t, err)
+	err = os.RemoveAll(user.HomeDir)
+	assert.NoError(t, err)
 
 	admin.Username += "1"
 	admin.Status = 1

+ 2 - 0
httpd/oidc_test.go

@@ -1097,6 +1097,8 @@ func TestOIDCPreLoginHook(t *testing.T) {
 
 	err = dataprovider.DeleteUser(username, "", "")
 	assert.NoError(t, err)
+	err = os.RemoveAll(u.HomeDir)
+	assert.NoError(t, err)
 
 	err = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, true), os.ModePerm)
 	assert.NoError(t, err)

File diff suppressed because it is too large
+ 0 - 0
static/vendor/video-js/video-js.min.css


File diff suppressed because it is too large
+ 11 - 0
static/vendor/video-js/video.min.js


+ 58 - 0
templates/webclient/files.html

@@ -24,6 +24,7 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
 <link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
 <link href="{{.StaticURL}}/vendor/datatables/dataTables.checkboxes.css" rel="stylesheet">
 <link href="{{.StaticURL}}/vendor/lightbox2/css/lightbox.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/video-js/video-js.min.css" rel="stylesheet" />
 <style>
     div.dataTables_wrapper span.selected-info,
     div.dataTables_wrapper span.selected-item {
@@ -185,6 +186,27 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
     </div>
 </div>
 
+<div class="modal fade" id="videoModal" tabindex="-1" role="dialog" aria-labelledby="videoModalLabel"
+    aria-hidden="true">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title" id="videoModalLabel">
+                    <span id="video_title"></span>
+                </h5>
+                <button class="close" type="button" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <video id="video_player" class="video-js vjs-big-play-centered vjs-fluid">
+                    <p class="vjs-no-js">To view this video please enable JavaScript, and consider upgrading to a web browser that supports HTML5 video</p>
+                  </video>
+            </div>
+        </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>
@@ -206,6 +228,7 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
 <script src="{{.StaticURL}}/vendor/pdfobject/pdfobject.min.js"></script>
 <script src="{{.StaticURL}}/vendor/codemirror/codemirror.js"></script>
 <script src="{{.StaticURL}}/vendor/codemirror/meta.js"></script>
+<script src="{{.StaticURL}}/vendor/video-js/video.min.js"></script>
 {{if .HasIntegrations}}
 <script type="text/javascript">
     var childReference = null;
@@ -407,6 +430,8 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
 {{end}}
 <script type="text/javascript">
     var spinnerDone = false;
+    var player;
+    var playerKeepAlive;
 
     var escapeHTML = function ( t ) {
 		return t
@@ -429,6 +454,17 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
         return shortened+'&#8230;';
     }
 
+    function openVideoPlayer(name, url, videoType){
+        $("#video_title").text(name);
+        $('#videoModal').modal('show');
+        player.src({
+            type: videoType,
+            src: url
+        });
+        keepAlive();
+        playerKeepAlive = setInterval(keepAlive, 300000);
+    }
+
     function getIconForFile(filename) {
         var extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
         switch (extension) {
@@ -460,6 +496,7 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
             case "mpeg":
             case "mpv":
             case "3gp":
+            case "mp4":
                 return "far fa-file-video";
             case "jpeg":
             case "jpg":
@@ -622,6 +659,19 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
     }
 
     $(document).ready(function () {
+        player = videojs('video_player', {
+            controls: true,
+            autoplay: false,
+            preload: 'auto'
+        });
+        $('#videoModal').on('hide.bs.modal', function () {
+            player.pause();
+            player.reset();
+            if (playerKeepAlive != null){
+                clearInterval(playerKeepAlive);
+            }
+        });
+
         $('#spinnerModal').on('shown.bs.modal', function () {
             if (spinnerDone){
                 $('#spinnerModal').modal('hide');
@@ -998,6 +1048,14 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
                                     case "ico":
                                         var view_url = row['url']+"&inline=1";
                                         return `<a href="${view_url}" data-lightbox="${filename}" data-title="${filename}"><i class="fas fa-eye"></i></a>`;
+                                    case "mp4":
+                                    case "mov":
+                                        return `<a href="#" onclick="openVideoPlayer('${row["name"]}', '${row['url']}', 'video/mp4');"><i class="fas fa-eye"></i></a>`;
+                                    case "webm":
+                                        return `<a href="#" onclick="openVideoPlayer('${row["name"]}', '${row['url']}', 'video/webm');"><i class="fas fa-eye"></i></a>`;
+                                    case "ogv":
+                                    case "ogg":
+                                        return `<a href="#" onclick="openVideoPlayer('${row["name"]}', '${row['url']}', 'video/ogg');"><i class="fas fa-eye"></i></a>`;
                                     case "pdf":
                                         if (PDFObject.supportsPDFs){
                                             var view_url = row['url'];

+ 1 - 0
templates/webclient/sharefiles.html

@@ -147,6 +147,7 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
             case "mpeg":
             case "mpv":
             case "3gp":
+            case "mp4":
                 return "far fa-file-video";
             case "jpeg":
             case "jpg":

+ 6 - 8
vfs/sftpfs.go

@@ -295,9 +295,6 @@ func (fs *SFTPFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, f
 	if err != nil {
 		return nil, nil, nil, err
 	}
-	if fs.config.BufferSize == 0 {
-		return f, nil, nil, err
-	}
 	if offset > 0 {
 		_, err = f.Seek(offset, io.SeekStart)
 		if err != nil {
@@ -305,6 +302,9 @@ func (fs *SFTPFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, f
 			return nil, nil, nil, err
 		}
 	}
+	if fs.config.BufferSize == 0 {
+		return f, nil, nil, nil
+	}
 	r, w, err := pipeat.PipeInDir(fs.localTempDir)
 	if err != nil {
 		f.Close()
@@ -507,11 +507,9 @@ func (*SFTPFs) IsNotSupported(err error) bool {
 
 // CheckRootPath creates the specified local root directory if it does not exists
 func (fs *SFTPFs) CheckRootPath(username string, uid int, gid int) bool {
-	if fs.config.BufferSize > 0 {
-		// we need a local directory for temporary files
-		osFs := NewOsFs(fs.ConnectionID(), fs.localTempDir, "")
-		osFs.CheckRootPath(username, uid, gid)
-	}
+	// local directory for temporary files in buffer mode
+	osFs := NewOsFs(fs.ConnectionID(), fs.localTempDir, "")
+	osFs.CheckRootPath(username, uid, gid)
 	if fs.config.Prefix == "/" {
 		return true
 	}

Some files were not shown because too many files changed in this diff