web client: allow to preview images and pdf
pdf depends on browser support. It does not work on mobile devices.
This commit is contained in:
parent
fc048728d9
commit
3f3591bae0
20 changed files with 208 additions and 54 deletions
|
@ -155,7 +155,8 @@ func getUserFile(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if status, err := downloadFile(w, r, connection, name, info); err != nil {
|
inline := r.URL.Query().Get("inline") != ""
|
||||||
|
if status, err := downloadFile(w, r, connection, name, info, inline); err != nil {
|
||||||
resp := apiResponse{
|
resp := apiResponse{
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
Message: http.StatusText(status),
|
Message: http.StatusText(status),
|
||||||
|
|
|
@ -257,7 +257,9 @@ func getZipEntryName(entryPath, baseDir string) string {
|
||||||
return strings.TrimPrefix(entryPath, "/")
|
return strings.TrimPrefix(entryPath, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadFile(w http.ResponseWriter, r *http.Request, connection *Connection, name string, info os.FileInfo) (int, error) {
|
func downloadFile(w http.ResponseWriter, r *http.Request, connection *Connection, name string,
|
||||||
|
info os.FileInfo, inline bool,
|
||||||
|
) (int, error) {
|
||||||
var err error
|
var err error
|
||||||
rangeHeader := r.Header.Get("Range")
|
rangeHeader := r.Header.Get("Range")
|
||||||
if rangeHeader != "" && checkIfRange(r, info.ModTime()) == condFalse {
|
if rangeHeader != "" && checkIfRange(r, info.ModTime()) == condFalse {
|
||||||
|
@ -295,7 +297,9 @@ func downloadFile(w http.ResponseWriter, r *http.Request, connection *Connection
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
|
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
|
||||||
w.Header().Set("Content-Type", ctype)
|
w.Header().Set("Content-Type", ctype)
|
||||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%#v", path.Base(name)))
|
if !inline {
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%#v", path.Base(name)))
|
||||||
|
}
|
||||||
w.Header().Set("Accept-Ranges", "bytes")
|
w.Header().Set("Accept-Ranges", "bytes")
|
||||||
w.WriteHeader(responseStatus)
|
w.WriteHeader(responseStatus)
|
||||||
if r.Method != http.MethodHead {
|
if r.Method != http.MethodHead {
|
||||||
|
|
|
@ -136,13 +136,14 @@ const (
|
||||||
webClientPubSharesPathDefault = "/web/client/pubshares"
|
webClientPubSharesPathDefault = "/web/client/pubshares"
|
||||||
webClientForgotPwdPathDefault = "/web/client/forgot-password"
|
webClientForgotPwdPathDefault = "/web/client/forgot-password"
|
||||||
webClientResetPwdPathDefault = "/web/client/reset-password"
|
webClientResetPwdPathDefault = "/web/client/reset-password"
|
||||||
|
webClientViewPDFPathDefault = "/web/client/viewpdf"
|
||||||
webStaticFilesPathDefault = "/static"
|
webStaticFilesPathDefault = "/static"
|
||||||
webOpenAPIPathDefault = "/openapi"
|
webOpenAPIPathDefault = "/openapi"
|
||||||
// MaxRestoreSize defines the max size for the loaddata input file
|
// MaxRestoreSize defines the max size for the loaddata input file
|
||||||
MaxRestoreSize = 10485760 // 10 MB
|
MaxRestoreSize = 10485760 // 10 MB
|
||||||
maxRequestSize = 1048576 // 1MB
|
maxRequestSize = 1048576 // 1MB
|
||||||
maxLoginBodySize = 262144 // 256 KB
|
maxLoginBodySize = 262144 // 256 KB
|
||||||
httpdMaxEditFileSize = 524288 // 512 KB
|
httpdMaxEditFileSize = 1048576 // 1 MB
|
||||||
maxMultipartMem = 8388608 // 8MB
|
maxMultipartMem = 8388608 // 8MB
|
||||||
osWindows = "windows"
|
osWindows = "windows"
|
||||||
otpHeaderCode = "X-SFTPGO-OTP"
|
otpHeaderCode = "X-SFTPGO-OTP"
|
||||||
|
@ -210,6 +211,7 @@ var (
|
||||||
webClientLogoutPath string
|
webClientLogoutPath string
|
||||||
webClientForgotPwdPath string
|
webClientForgotPwdPath string
|
||||||
webClientResetPwdPath string
|
webClientResetPwdPath string
|
||||||
|
webClientViewPDFPath string
|
||||||
webStaticFilesPath string
|
webStaticFilesPath string
|
||||||
webOpenAPIPath string
|
webOpenAPIPath string
|
||||||
// max upload size for http clients, 1GB by default
|
// max upload size for http clients, 1GB by default
|
||||||
|
@ -570,6 +572,7 @@ func updateWebClientURLs(baseURL string) {
|
||||||
webClientRecoveryCodesPath = path.Join(baseURL, webClientRecoveryCodesPathDefault)
|
webClientRecoveryCodesPath = path.Join(baseURL, webClientRecoveryCodesPathDefault)
|
||||||
webClientForgotPwdPath = path.Join(baseURL, webClientForgotPwdPathDefault)
|
webClientForgotPwdPath = path.Join(baseURL, webClientForgotPwdPathDefault)
|
||||||
webClientResetPwdPath = path.Join(baseURL, webClientResetPwdPathDefault)
|
webClientResetPwdPath = path.Join(baseURL, webClientResetPwdPathDefault)
|
||||||
|
webClientViewPDFPath = path.Join(baseURL, webClientViewPDFPathDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateWebAdminURLs(baseURL string) {
|
func updateWebAdminURLs(baseURL string) {
|
||||||
|
|
|
@ -153,6 +153,7 @@ const (
|
||||||
webClientPubSharesPath = "/web/client/pubshares"
|
webClientPubSharesPath = "/web/client/pubshares"
|
||||||
webClientForgotPwdPath = "/web/client/forgot-password"
|
webClientForgotPwdPath = "/web/client/forgot-password"
|
||||||
webClientResetPwdPath = "/web/client/reset-password"
|
webClientResetPwdPath = "/web/client/reset-password"
|
||||||
|
webClientViewPDFPath = "/web/client/viewpdf"
|
||||||
httpBaseURL = "http://127.0.0.1:8081"
|
httpBaseURL = "http://127.0.0.1:8081"
|
||||||
sftpServerAddr = "127.0.0.1:8022"
|
sftpServerAddr = "127.0.0.1:8022"
|
||||||
smtpServerAddr = "127.0.0.1:3525"
|
smtpServerAddr = "127.0.0.1:3525"
|
||||||
|
@ -9320,13 +9321,38 @@ func TestUserAPIKey(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWebClientViewPDF(t *testing.T) {
|
||||||
|
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
webToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, webClientViewPDFPath, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
setJWTCookieForReq(req, webToken)
|
||||||
|
rr := executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||||
|
|
||||||
|
req, err = http.NewRequest(http.MethodGet, webClientViewPDFPath+"?path=test.pdf", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
setJWTCookieForReq(req, webToken)
|
||||||
|
rr = executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusOK, rr)
|
||||||
|
|
||||||
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestWebEditFile(t *testing.T) {
|
func TestWebEditFile(t *testing.T) {
|
||||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
testFile1 := "testfile1.txt"
|
testFile1 := "testfile1.txt"
|
||||||
testFile2 := "testfile2"
|
testFile2 := "testfile2"
|
||||||
file1Size := int64(65536)
|
file1Size := int64(65536)
|
||||||
file2Size := int64(655360)
|
file2Size := int64(1048576 * 2)
|
||||||
err = createTestFile(filepath.Join(user.GetHomeDir(), testFile1), file1Size)
|
err = createTestFile(filepath.Join(user.GetHomeDir(), testFile1), file1Size)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = createTestFile(filepath.Join(user.GetHomeDir(), testFile2), file2Size)
|
err = createTestFile(filepath.Join(user.GetHomeDir(), testFile2), file2Size)
|
||||||
|
|
|
@ -1223,10 +1223,10 @@ func (s *httpdServer) initializeRouter() {
|
||||||
|
|
||||||
router.Get(webClientLogoutPath, handleWebClientLogout)
|
router.Get(webClientLogoutPath, handleWebClientLogout)
|
||||||
router.With(s.refreshCookie).Get(webClientFilesPath, handleClientGetFiles)
|
router.With(s.refreshCookie).Get(webClientFilesPath, handleClientGetFiles)
|
||||||
|
router.With(s.refreshCookie).Get(webClientViewPDFPath, handleClientViewPDF)
|
||||||
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||||
Post(webClientFilesPath, uploadUserFiles)
|
Post(webClientFilesPath, uploadUserFiles)
|
||||||
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.refreshCookie).
|
router.With(s.refreshCookie).Get(webClientEditFilePath, handleClientEditFile)
|
||||||
Get(webClientEditFilePath, handleClientEditFile)
|
|
||||||
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||||
Patch(webClientFilesPath, renameUserFile)
|
Patch(webClientFilesPath, renameUserFile)
|
||||||
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||||
|
|
|
@ -44,6 +44,7 @@ const (
|
||||||
templateClientEditFile = "editfile.html"
|
templateClientEditFile = "editfile.html"
|
||||||
templateClientShare = "share.html"
|
templateClientShare = "share.html"
|
||||||
templateClientShares = "shares.html"
|
templateClientShares = "shares.html"
|
||||||
|
templateClientViewPDF = "viewpdf.html"
|
||||||
pageClientFilesTitle = "My Files"
|
pageClientFilesTitle = "My Files"
|
||||||
pageClientSharesTitle = "Shares"
|
pageClientSharesTitle = "Shares"
|
||||||
pageClientProfileTitle = "My Profile"
|
pageClientProfileTitle = "My Profile"
|
||||||
|
@ -99,11 +100,18 @@ type dirMapping struct {
|
||||||
Href string
|
Href string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type viewPDFPage struct {
|
||||||
|
Title string
|
||||||
|
URL string
|
||||||
|
StaticURL string
|
||||||
|
}
|
||||||
|
|
||||||
type editFilePage struct {
|
type editFilePage struct {
|
||||||
baseClientPage
|
baseClientPage
|
||||||
CurrentDir string
|
CurrentDir string
|
||||||
Path string
|
Path string
|
||||||
Name string
|
Name string
|
||||||
|
ReadOnly bool
|
||||||
Data string
|
Data string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +120,7 @@ type filesPage struct {
|
||||||
CurrentDir string
|
CurrentDir string
|
||||||
DirsURL string
|
DirsURL string
|
||||||
DownloadURL string
|
DownloadURL string
|
||||||
|
ViewPDFURL string
|
||||||
CanAddFiles bool
|
CanAddFiles bool
|
||||||
CanCreateDirs bool
|
CanCreateDirs bool
|
||||||
CanRename bool
|
CanRename bool
|
||||||
|
@ -229,6 +238,9 @@ func loadClientTemplates(templatesPath string) {
|
||||||
resetPwdPaths := []string{
|
resetPwdPaths := []string{
|
||||||
filepath.Join(templatesPath, templateCommonDir, templateResetPassword),
|
filepath.Join(templatesPath, templateCommonDir, templateResetPassword),
|
||||||
}
|
}
|
||||||
|
viewPDFPaths := []string{
|
||||||
|
filepath.Join(templatesPath, templateClientDir, templateClientViewPDF),
|
||||||
|
}
|
||||||
|
|
||||||
filesTmpl := util.LoadTemplate(nil, filesPaths...)
|
filesTmpl := util.LoadTemplate(nil, filesPaths...)
|
||||||
profileTmpl := util.LoadTemplate(nil, profilePaths...)
|
profileTmpl := util.LoadTemplate(nil, profilePaths...)
|
||||||
|
@ -243,6 +255,7 @@ func loadClientTemplates(templatesPath string) {
|
||||||
shareTmpl := util.LoadTemplate(nil, sharePaths...)
|
shareTmpl := util.LoadTemplate(nil, sharePaths...)
|
||||||
forgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...)
|
forgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...)
|
||||||
resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
|
resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
|
||||||
|
viewPDFTmpl := util.LoadTemplate(nil, viewPDFPaths...)
|
||||||
|
|
||||||
clientTemplates[templateClientFiles] = filesTmpl
|
clientTemplates[templateClientFiles] = filesTmpl
|
||||||
clientTemplates[templateClientProfile] = profileTmpl
|
clientTemplates[templateClientProfile] = profileTmpl
|
||||||
|
@ -257,6 +270,7 @@ func loadClientTemplates(templatesPath string) {
|
||||||
clientTemplates[templateClientShare] = shareTmpl
|
clientTemplates[templateClientShare] = shareTmpl
|
||||||
clientTemplates[templateForgotPassword] = forgotPwdTmpl
|
clientTemplates[templateForgotPassword] = forgotPwdTmpl
|
||||||
clientTemplates[templateResetPassword] = resetPwdTmpl
|
clientTemplates[templateResetPassword] = resetPwdTmpl
|
||||||
|
clientTemplates[templateClientViewPDF] = viewPDFTmpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage {
|
func getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage {
|
||||||
|
@ -391,12 +405,13 @@ func renderClientMFAPage(w http.ResponseWriter, r *http.Request) {
|
||||||
renderClientTemplate(w, templateClientMFA, data)
|
renderClientTemplate(w, templateClientMFA, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileData string) {
|
func renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileData string, readOnly bool) {
|
||||||
data := editFilePage{
|
data := editFilePage{
|
||||||
baseClientPage: getBaseClientPageData(pageClientEditFileTitle, webClientEditFilePath, r),
|
baseClientPage: getBaseClientPageData(pageClientEditFileTitle, webClientEditFilePath, r),
|
||||||
Path: fileName,
|
Path: fileName,
|
||||||
Name: path.Base(fileName),
|
Name: path.Base(fileName),
|
||||||
CurrentDir: path.Dir(fileName),
|
CurrentDir: path.Dir(fileName),
|
||||||
|
ReadOnly: readOnly,
|
||||||
Data: fileData,
|
Data: fileData,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,6 +442,7 @@ func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error stri
|
||||||
Error: error,
|
Error: error,
|
||||||
CurrentDir: url.QueryEscape(dirName),
|
CurrentDir: url.QueryEscape(dirName),
|
||||||
DownloadURL: webClientDownloadZipPath,
|
DownloadURL: webClientDownloadZipPath,
|
||||||
|
ViewPDFURL: webClientViewPDFPath,
|
||||||
DirsURL: webClientDirsPath,
|
DirsURL: webClientDirsPath,
|
||||||
CanAddFiles: user.CanAddFilesFromWeb(dirName),
|
CanAddFiles: user.CanAddFilesFromWeb(dirName),
|
||||||
CanCreateDirs: user.CanAddDirsFromWeb(dirName),
|
CanCreateDirs: user.CanAddDirsFromWeb(dirName),
|
||||||
|
@ -650,7 +666,8 @@ func handleClientGetFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
renderFilesPage(w, r, name, "", user)
|
renderFilesPage(w, r, name, "", user)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if status, err := downloadFile(w, r, connection, name, info); err != nil && status != 0 {
|
inline := r.URL.Query().Get("inline") != ""
|
||||||
|
if status, err := downloadFile(w, r, connection, name, info, inline); err != nil && status != 0 {
|
||||||
if status > 0 {
|
if status > 0 {
|
||||||
if status == http.StatusRequestedRangeNotSatisfiable {
|
if status == http.StatusRequestedRangeNotSatisfiable {
|
||||||
renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "")
|
renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "")
|
||||||
|
@ -723,7 +740,7 @@ func handleClientEditFile(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEditFilePage(w, r, name, b.String())
|
renderEditFilePage(w, r, name, b.String(), util.IsStringInSlice(sdk.WebClientWriteDisabled, user.Filters.WebClient))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {
|
func handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -1035,3 +1052,19 @@ func handleWebClientPasswordReset(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
renderClientResetPwdPage(w, "")
|
renderClientResetPwdPage(w, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleClientViewPDF(w http.ResponseWriter, r *http.Request) {
|
||||||
|
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||||
|
name := r.URL.Query().Get("path")
|
||||||
|
if name == "" {
|
||||||
|
renderClientBadRequestPage(w, r, errors.New("no file specified"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name = util.CleanPath(name)
|
||||||
|
data := viewPDFPage{
|
||||||
|
Title: path.Base(name),
|
||||||
|
URL: fmt.Sprintf("%v?path=%v&inline=1", webClientFilesPath, url.QueryEscape(name)),
|
||||||
|
StaticURL: webStaticFilesPath,
|
||||||
|
}
|
||||||
|
renderClientTemplate(w, templateClientViewPDF, data)
|
||||||
|
}
|
||||||
|
|
|
@ -3546,6 +3546,12 @@ paths:
|
||||||
description: Path to the file to download. It must be URL encoded, for example the path "my dir/àdir/file.txt" must be sent as "my%20dir%2F%C3%A0dir%2Ffile.txt"
|
description: Path to the file to download. It must be URL encoded, for example the path "my dir/àdir/file.txt" must be sent as "my%20dir%2F%C3%A0dir%2Ffile.txt"
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: inline
|
||||||
|
required: false
|
||||||
|
description: 'If set, the response will not have the Content-Disposition header set to `attachment`'
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: successful operation
|
description: successful operation
|
||||||
|
|
|
@ -202,6 +202,7 @@
|
||||||
|
|
||||||
function SearchCursor(doc, query, pos, options) {
|
function SearchCursor(doc, query, pos, options) {
|
||||||
this.atOccurrence = false
|
this.atOccurrence = false
|
||||||
|
this.afterEmptyMatch = false
|
||||||
this.doc = doc
|
this.doc = doc
|
||||||
pos = pos ? doc.clipPos(pos) : Pos(0, 0)
|
pos = pos ? doc.clipPos(pos) : Pos(0, 0)
|
||||||
this.pos = {from: pos, to: pos}
|
this.pos = {from: pos, to: pos}
|
||||||
|
@ -237,21 +238,29 @@
|
||||||
findPrevious: function() {return this.find(true)},
|
findPrevious: function() {return this.find(true)},
|
||||||
|
|
||||||
find: function(reverse) {
|
find: function(reverse) {
|
||||||
var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to))
|
var head = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
|
||||||
|
if (this.afterEmptyMatch && this.atOccurrence) {
|
||||||
// Implements weird auto-growing behavior on null-matches for
|
// do not return the same 0 width match twice
|
||||||
// backwards-compatibility with the vim code (unfortunately)
|
head = Pos(head.line, head.ch)
|
||||||
while (result && CodeMirror.cmpPos(result.from, result.to) == 0) {
|
|
||||||
if (reverse) {
|
if (reverse) {
|
||||||
if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1)
|
head.ch--;
|
||||||
else if (result.from.line == this.doc.firstLine()) result = null
|
if (head.ch < 0) {
|
||||||
else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1)))
|
head.line--;
|
||||||
|
head.ch = (this.doc.getLine(head.line) || "").length;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1)
|
head.ch++;
|
||||||
else if (result.to.line == this.doc.lastLine()) result = null
|
if (head.ch > (this.doc.getLine(head.line) || "").length) {
|
||||||
else result = this.matches(reverse, Pos(result.to.line + 1, 0))
|
head.ch = 0;
|
||||||
|
head.line++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (CodeMirror.cmpPos(head, this.doc.clipPos(head)) != 0) {
|
||||||
|
return this.atOccurrence = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var result = this.matches(reverse, head)
|
||||||
|
this.afterEmptyMatch = result && CodeMirror.cmpPos(result.from, result.to) == 0
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
this.pos = result
|
this.pos = result
|
||||||
|
|
20
static/vendor/codemirror/codemirror.css
vendored
20
static/vendor/codemirror/codemirror.css
vendored
|
@ -60,19 +60,13 @@
|
||||||
.cm-fat-cursor div.CodeMirror-cursors {
|
.cm-fat-cursor div.CodeMirror-cursors {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.cm-fat-cursor-mark {
|
.cm-fat-cursor .CodeMirror-line::selection,
|
||||||
background-color: rgba(20, 255, 20, 0.5);
|
.cm-fat-cursor .CodeMirror-line > span::selection,
|
||||||
-webkit-animation: blink 1.06s steps(1) infinite;
|
.cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; }
|
||||||
-moz-animation: blink 1.06s steps(1) infinite;
|
.cm-fat-cursor .CodeMirror-line::-moz-selection,
|
||||||
animation: blink 1.06s steps(1) infinite;
|
.cm-fat-cursor .CodeMirror-line > span::-moz-selection,
|
||||||
}
|
.cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; }
|
||||||
.cm-animate-fat-cursor {
|
.cm-fat-cursor { caret-color: transparent; }
|
||||||
width: auto;
|
|
||||||
-webkit-animation: blink 1.06s steps(1) infinite;
|
|
||||||
-moz-animation: blink 1.06s steps(1) infinite;
|
|
||||||
animation: blink 1.06s steps(1) infinite;
|
|
||||||
background-color: #7e7;
|
|
||||||
}
|
|
||||||
@-moz-keyframes blink {
|
@-moz-keyframes blink {
|
||||||
0% {}
|
0% {}
|
||||||
50% { background-color: transparent; }
|
50% { background-color: transparent; }
|
||||||
|
|
36
static/vendor/codemirror/codemirror.js
vendored
36
static/vendor/codemirror/codemirror.js
vendored
|
@ -2351,12 +2351,14 @@
|
||||||
function mapFromLineView(lineView, line, lineN) {
|
function mapFromLineView(lineView, line, lineN) {
|
||||||
if (lineView.line == line)
|
if (lineView.line == line)
|
||||||
{ return {map: lineView.measure.map, cache: lineView.measure.cache} }
|
{ return {map: lineView.measure.map, cache: lineView.measure.cache} }
|
||||||
for (var i = 0; i < lineView.rest.length; i++)
|
if (lineView.rest) {
|
||||||
{ if (lineView.rest[i] == line)
|
for (var i = 0; i < lineView.rest.length; i++)
|
||||||
{ return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } }
|
{ if (lineView.rest[i] == line)
|
||||||
for (var i$1 = 0; i$1 < lineView.rest.length; i$1++)
|
{ return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } }
|
||||||
{ if (lineNo(lineView.rest[i$1]) > lineN)
|
for (var i$1 = 0; i$1 < lineView.rest.length; i$1++)
|
||||||
{ return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } }
|
{ if (lineNo(lineView.rest[i$1]) > lineN)
|
||||||
|
{ return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render a line into the hidden node display.externalMeasured. Used
|
// Render a line into the hidden node display.externalMeasured. Used
|
||||||
|
@ -3150,13 +3152,19 @@
|
||||||
var curFragment = result.cursors = document.createDocumentFragment();
|
var curFragment = result.cursors = document.createDocumentFragment();
|
||||||
var selFragment = result.selection = document.createDocumentFragment();
|
var selFragment = result.selection = document.createDocumentFragment();
|
||||||
|
|
||||||
|
var customCursor = cm.options.$customCursor;
|
||||||
|
if (customCursor) { primary = true; }
|
||||||
for (var i = 0; i < doc.sel.ranges.length; i++) {
|
for (var i = 0; i < doc.sel.ranges.length; i++) {
|
||||||
if (!primary && i == doc.sel.primIndex) { continue }
|
if (!primary && i == doc.sel.primIndex) { continue }
|
||||||
var range = doc.sel.ranges[i];
|
var range = doc.sel.ranges[i];
|
||||||
if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) { continue }
|
if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) { continue }
|
||||||
var collapsed = range.empty();
|
var collapsed = range.empty();
|
||||||
if (collapsed || cm.options.showCursorWhenSelecting)
|
if (customCursor) {
|
||||||
{ drawSelectionCursor(cm, range.head, curFragment); }
|
var head = customCursor(cm, range);
|
||||||
|
if (head) { drawSelectionCursor(cm, head, curFragment); }
|
||||||
|
} else if (collapsed || cm.options.showCursorWhenSelecting) {
|
||||||
|
drawSelectionCursor(cm, range.head, curFragment);
|
||||||
|
}
|
||||||
if (!collapsed)
|
if (!collapsed)
|
||||||
{ drawSelectionRange(cm, range, selFragment); }
|
{ drawSelectionRange(cm, range, selFragment); }
|
||||||
}
|
}
|
||||||
|
@ -3174,9 +3182,8 @@
|
||||||
|
|
||||||
if (/\bcm-fat-cursor\b/.test(cm.getWrapperElement().className)) {
|
if (/\bcm-fat-cursor\b/.test(cm.getWrapperElement().className)) {
|
||||||
var charPos = charCoords(cm, head, "div", null, null);
|
var charPos = charCoords(cm, head, "div", null, null);
|
||||||
if (charPos.right - charPos.left > 0) {
|
var width = charPos.right - charPos.left;
|
||||||
cursor.style.width = (charPos.right - charPos.left) + "px";
|
cursor.style.width = (width > 0 ? width : cm.defaultCharWidth()) + "px";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pos.other) {
|
if (pos.other) {
|
||||||
|
@ -3649,6 +3656,7 @@
|
||||||
this.vert.firstChild.style.height =
|
this.vert.firstChild.style.height =
|
||||||
Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px";
|
Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px";
|
||||||
} else {
|
} else {
|
||||||
|
this.vert.scrollTop = 0;
|
||||||
this.vert.style.display = "";
|
this.vert.style.display = "";
|
||||||
this.vert.firstChild.style.height = "0";
|
this.vert.firstChild.style.height = "0";
|
||||||
}
|
}
|
||||||
|
@ -4501,7 +4509,7 @@
|
||||||
function onScrollWheel(cm, e) {
|
function onScrollWheel(cm, e) {
|
||||||
var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y;
|
var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y;
|
||||||
var pixelsPerUnit = wheelPixelsPerUnit;
|
var pixelsPerUnit = wheelPixelsPerUnit;
|
||||||
if (event.deltaMode === 0) {
|
if (e.deltaMode === 0) {
|
||||||
dx = e.deltaX;
|
dx = e.deltaX;
|
||||||
dy = e.deltaY;
|
dy = e.deltaY;
|
||||||
pixelsPerUnit = 1;
|
pixelsPerUnit = 1;
|
||||||
|
@ -8235,7 +8243,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function hiddenTextarea() {
|
function hiddenTextarea() {
|
||||||
var te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; outline: none");
|
var te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; min-height: 1em; outline: none");
|
||||||
var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
|
var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
|
||||||
// The textarea is kept positioned near the cursor to prevent the
|
// The textarea is kept positioned near the cursor to prevent the
|
||||||
// fact that it'll be scrolled into view on input from scrolling
|
// fact that it'll be scrolled into view on input from scrolling
|
||||||
|
@ -9832,7 +9840,7 @@
|
||||||
|
|
||||||
addLegacyProps(CodeMirror);
|
addLegacyProps(CodeMirror);
|
||||||
|
|
||||||
CodeMirror.version = "5.63.1";
|
CodeMirror.version = "5.64.0";
|
||||||
|
|
||||||
return CodeMirror;
|
return CodeMirror;
|
||||||
|
|
||||||
|
|
1
static/vendor/lightbox2/css/lightbox.min.css
vendored
Normal file
1
static/vendor/lightbox2/css/lightbox.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.lb-loader,.lightbox{text-align:center;line-height:0;position:absolute;left:0}body.lb-disable-scrolling{overflow:hidden}.lightboxOverlay{position:absolute;top:0;left:0;z-index:9999;background-color:#000;filter:alpha(Opacity=80);opacity:.8;display:none}.lightbox{width:100%;z-index:10000;font-weight:400;outline:0}.lightbox .lb-image{display:block;height:auto;max-width:inherit;max-height:none;border-radius:3px;border:4px solid #fff}.lightbox a img{border:none}.lb-outerContainer{position:relative;width:250px;height:250px;margin:0 auto;border-radius:4px;background-color:#fff}.lb-outerContainer:after{content:"";display:table;clear:both}.lb-loader{top:43%;height:25%;width:100%}.lb-cancel{display:block;width:32px;height:32px;margin:0 auto;background:url(../images/loading.gif) no-repeat}.lb-nav{position:absolute;top:0;left:0;height:100%;width:100%;z-index:10}.lb-container>.nav{left:0}.lb-nav a{outline:0;background-image:url(data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==)}.lb-next,.lb-prev{height:100%;cursor:pointer;display:block}.lb-nav a.lb-prev{width:34%;left:0;float:left;background:url(../images/prev.png) left 48% no-repeat;filter:alpha(Opacity=0);opacity:0;-webkit-transition:opacity .6s;-moz-transition:opacity .6s;-o-transition:opacity .6s;transition:opacity .6s}.lb-nav a.lb-prev:hover{filter:alpha(Opacity=100);opacity:1}.lb-nav a.lb-next{width:64%;right:0;float:right;background:url(../images/next.png) right 48% no-repeat;filter:alpha(Opacity=0);opacity:0;-webkit-transition:opacity .6s;-moz-transition:opacity .6s;-o-transition:opacity .6s;transition:opacity .6s}.lb-nav a.lb-next:hover{filter:alpha(Opacity=100);opacity:1}.lb-dataContainer{margin:0 auto;padding-top:5px;width:100%;border-bottom-left-radius:4px;border-bottom-right-radius:4px}.lb-dataContainer:after{content:"";display:table;clear:both}.lb-data{padding:0 4px;color:#ccc}.lb-data .lb-details{width:85%;float:left;text-align:left;line-height:1.1em}.lb-data .lb-caption{font-size:13px;font-weight:700;line-height:1em}.lb-data .lb-caption a{color:#4ae}.lb-data .lb-number{display:block;clear:left;padding-bottom:1em;font-size:12px;color:#999}.lb-data .lb-close{display:block;float:right;width:30px;height:30px;background:url(../images/close.png) top right no-repeat;text-align:right;outline:0;filter:alpha(Opacity=70);opacity:.7;-webkit-transition:opacity .2s;-moz-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}.lb-data .lb-close:hover{cursor:pointer;filter:alpha(Opacity=100);opacity:1}
|
BIN
static/vendor/lightbox2/images/close.png
vendored
Normal file
BIN
static/vendor/lightbox2/images/close.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 280 B |
BIN
static/vendor/lightbox2/images/loading.gif
vendored
Normal file
BIN
static/vendor/lightbox2/images/loading.gif
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
BIN
static/vendor/lightbox2/images/next.png
vendored
Normal file
BIN
static/vendor/lightbox2/images/next.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
static/vendor/lightbox2/images/prev.png
vendored
Normal file
BIN
static/vendor/lightbox2/images/prev.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
15
static/vendor/lightbox2/js/lightbox.min.js
vendored
Normal file
15
static/vendor/lightbox2/js/lightbox.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
9
static/vendor/pdfobject/pdfobject.min.js
vendored
Normal file
9
static/vendor/pdfobject/pdfobject.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -36,7 +36,9 @@
|
||||||
<span class="font-weight-bold text-primary">Edit file "{{.Path}}"</span>
|
<span class="font-weight-bold text-primary">Edit file "{{.Path}}"</span>
|
||||||
<span class="btn-toolbar">
|
<span class="btn-toolbar">
|
||||||
<a id="idBack" class="btn btn-secondary mx-1 my-1" href='{{.FilesURL}}?path={{.CurrentDir}}' role="button">Back</a>
|
<a id="idBack" class="btn btn-secondary mx-1 my-1" href='{{.FilesURL}}?path={{.CurrentDir}}' role="button">Back</a>
|
||||||
|
{{if not .ReadOnly}}
|
||||||
<a id="idSave" class="btn btn-primary mx-1 my-1" href="#" onclick="saveFile()" role="button">Save</a>
|
<a id="idSave" class="btn btn-primary mx-1 my-1" href="#" onclick="saveFile()" role="button">Save</a>
|
||||||
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
</h6>
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,6 +109,9 @@
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
styleActiveLine: true,
|
styleActiveLine: true,
|
||||||
extraKeys: {"Alt-F": "findPersistent"},
|
extraKeys: {"Alt-F": "findPersistent"},
|
||||||
|
{{if .ReadOnly}}
|
||||||
|
readOnly: true,
|
||||||
|
{{end}}
|
||||||
autofocus: true
|
autofocus: true
|
||||||
});
|
});
|
||||||
var filename = "{{.Path}}";
|
var filename = "{{.Path}}";
|
||||||
|
@ -126,6 +131,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{{if not .ReadOnly}}
|
||||||
function saveFile() {
|
function saveFile() {
|
||||||
$('#idSave').addClass("disabled");
|
$('#idSave').addClass("disabled");
|
||||||
cm = document.querySelector('.CodeMirror').CodeMirror;
|
cm = document.querySelector('.CodeMirror').CodeMirror;
|
||||||
|
@ -167,5 +173,6 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
{{end}}
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
|
@ -8,6 +8,7 @@
|
||||||
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
|
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
|
||||||
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
|
<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/datatables/dataTables.checkboxes.css" rel="stylesheet">
|
||||||
|
<link href="{{.StaticURL}}/vendor/lightbox2/css/lightbox.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
div.dataTables_wrapper span.selected-info,
|
div.dataTables_wrapper span.selected-info,
|
||||||
div.dataTables_wrapper span.selected-item {
|
div.dataTables_wrapper span.selected-item {
|
||||||
|
@ -177,10 +178,10 @@
|
||||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
|
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
|
||||||
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
|
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
|
||||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.checkboxes.min.js"></script>
|
<script src="{{.StaticURL}}/vendor/datatables/dataTables.checkboxes.min.js"></script>
|
||||||
{{if .CanAddFiles}}
|
<script src="{{.StaticURL}}/vendor/lightbox2/js/lightbox.min.js"></script>
|
||||||
|
<script src="{{.StaticURL}}/vendor/pdfobject/pdfobject.min.js"></script>
|
||||||
<script src="{{.StaticURL}}/vendor/codemirror/codemirror.js"></script>
|
<script src="{{.StaticURL}}/vendor/codemirror/codemirror.js"></script>
|
||||||
<script src="{{.StaticURL}}/vendor/codemirror/meta.js"></script>
|
<script src="{{.StaticURL}}/vendor/codemirror/meta.js"></script>
|
||||||
{{end}}
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
function getIconForFile(filename) {
|
function getIconForFile(filename) {
|
||||||
|
@ -613,15 +614,37 @@
|
||||||
{ "data": "edit_url",
|
{ "data": "edit_url",
|
||||||
"render": function (data, type, row) {
|
"render": function (data, type, row) {
|
||||||
if (type === 'display') {
|
if (type === 'display') {
|
||||||
{{if .CanAddFiles}}
|
var filename = row["name"];
|
||||||
|
var extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
|
||||||
if (data != ""){
|
if (data != ""){
|
||||||
var filename = row["name"];
|
|
||||||
var extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
|
|
||||||
if (extension == "csv" || extension == "bat" || CodeMirror.findModeByExtension(extension) != null){
|
if (extension == "csv" || extension == "bat" || CodeMirror.findModeByExtension(extension) != null){
|
||||||
|
{{if .CanAddFiles}}
|
||||||
return `<a href="${data}"><i class="fas fa-edit"></i></a>`;
|
return `<a href="${data}"><i class="fas fa-edit"></i></a>`;
|
||||||
|
{{else}}
|
||||||
|
return `<a href="${data}"><i class="fas fa-eye"></i></a>`;
|
||||||
|
{{end}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (row["type"] == "2") {
|
||||||
|
switch (extension) {
|
||||||
|
case "jpeg":
|
||||||
|
case "jpg":
|
||||||
|
case "png":
|
||||||
|
case "gif":
|
||||||
|
case "webp":
|
||||||
|
case "bmp":
|
||||||
|
case "svg":
|
||||||
|
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 "pdf":
|
||||||
|
if (PDFObject.supportsPDFs){
|
||||||
|
var view_url = row['url'];
|
||||||
|
view_url = view_url.replace('{{.FilesURL}}','{{.ViewPDFURL}}');
|
||||||
|
return `<a href="${view_url}" target="_blank"><i class="fas fa-eye"></i></a>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{{end}}
|
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
15
templates/webclient/viewpdf.html
Normal file
15
templates/webclient/viewpdf.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<script src="{{.StaticURL}}/vendor/pdfobject/pdfobject.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
PDFObject.embed("{{.URL}}", document.body);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue