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:
Nicola Murino 2021-11-25 19:24:32 +01:00
parent fc048728d9
commit 3f3591bae0
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
20 changed files with 208 additions and 54 deletions

View file

@ -155,7 +155,8 @@ func getUserFile(w http.ResponseWriter, r *http.Request) {
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{
Error: err.Error(),
Message: http.StatusText(status),

View file

@ -257,7 +257,9 @@ func getZipEntryName(entryPath, baseDir string) string {
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
rangeHeader := r.Header.Get("Range")
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-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.WriteHeader(responseStatus)
if r.Method != http.MethodHead {

View file

@ -136,13 +136,14 @@ const (
webClientPubSharesPathDefault = "/web/client/pubshares"
webClientForgotPwdPathDefault = "/web/client/forgot-password"
webClientResetPwdPathDefault = "/web/client/reset-password"
webClientViewPDFPathDefault = "/web/client/viewpdf"
webStaticFilesPathDefault = "/static"
webOpenAPIPathDefault = "/openapi"
// MaxRestoreSize defines the max size for the loaddata input file
MaxRestoreSize = 10485760 // 10 MB
maxRequestSize = 1048576 // 1MB
maxLoginBodySize = 262144 // 256 KB
httpdMaxEditFileSize = 524288 // 512 KB
httpdMaxEditFileSize = 1048576 // 1 MB
maxMultipartMem = 8388608 // 8MB
osWindows = "windows"
otpHeaderCode = "X-SFTPGO-OTP"
@ -210,6 +211,7 @@ var (
webClientLogoutPath string
webClientForgotPwdPath string
webClientResetPwdPath string
webClientViewPDFPath string
webStaticFilesPath string
webOpenAPIPath string
// max upload size for http clients, 1GB by default
@ -570,6 +572,7 @@ func updateWebClientURLs(baseURL string) {
webClientRecoveryCodesPath = path.Join(baseURL, webClientRecoveryCodesPathDefault)
webClientForgotPwdPath = path.Join(baseURL, webClientForgotPwdPathDefault)
webClientResetPwdPath = path.Join(baseURL, webClientResetPwdPathDefault)
webClientViewPDFPath = path.Join(baseURL, webClientViewPDFPathDefault)
}
func updateWebAdminURLs(baseURL string) {

View file

@ -153,6 +153,7 @@ const (
webClientPubSharesPath = "/web/client/pubshares"
webClientForgotPwdPath = "/web/client/forgot-password"
webClientResetPwdPath = "/web/client/reset-password"
webClientViewPDFPath = "/web/client/viewpdf"
httpBaseURL = "http://127.0.0.1:8081"
sftpServerAddr = "127.0.0.1:8022"
smtpServerAddr = "127.0.0.1:3525"
@ -9320,13 +9321,38 @@ func TestUserAPIKey(t *testing.T) {
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) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
testFile1 := "testfile1.txt"
testFile2 := "testfile2"
file1Size := int64(65536)
file2Size := int64(655360)
file2Size := int64(1048576 * 2)
err = createTestFile(filepath.Join(user.GetHomeDir(), testFile1), file1Size)
assert.NoError(t, err)
err = createTestFile(filepath.Join(user.GetHomeDir(), testFile2), file2Size)

View file

@ -1223,10 +1223,10 @@ func (s *httpdServer) initializeRouter() {
router.Get(webClientLogoutPath, handleWebClientLogout)
router.With(s.refreshCookie).Get(webClientFilesPath, handleClientGetFiles)
router.With(s.refreshCookie).Get(webClientViewPDFPath, handleClientViewPDF)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
Post(webClientFilesPath, uploadUserFiles)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), s.refreshCookie).
Get(webClientEditFilePath, handleClientEditFile)
router.With(s.refreshCookie).Get(webClientEditFilePath, handleClientEditFile)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
Patch(webClientFilesPath, renameUserFile)
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).

View file

@ -44,6 +44,7 @@ const (
templateClientEditFile = "editfile.html"
templateClientShare = "share.html"
templateClientShares = "shares.html"
templateClientViewPDF = "viewpdf.html"
pageClientFilesTitle = "My Files"
pageClientSharesTitle = "Shares"
pageClientProfileTitle = "My Profile"
@ -99,11 +100,18 @@ type dirMapping struct {
Href string
}
type viewPDFPage struct {
Title string
URL string
StaticURL string
}
type editFilePage struct {
baseClientPage
CurrentDir string
Path string
Name string
ReadOnly bool
Data string
}
@ -112,6 +120,7 @@ type filesPage struct {
CurrentDir string
DirsURL string
DownloadURL string
ViewPDFURL string
CanAddFiles bool
CanCreateDirs bool
CanRename bool
@ -229,6 +238,9 @@ func loadClientTemplates(templatesPath string) {
resetPwdPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateResetPassword),
}
viewPDFPaths := []string{
filepath.Join(templatesPath, templateClientDir, templateClientViewPDF),
}
filesTmpl := util.LoadTemplate(nil, filesPaths...)
profileTmpl := util.LoadTemplate(nil, profilePaths...)
@ -243,6 +255,7 @@ func loadClientTemplates(templatesPath string) {
shareTmpl := util.LoadTemplate(nil, sharePaths...)
forgotPwdTmpl := util.LoadTemplate(nil, forgotPwdPaths...)
resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
viewPDFTmpl := util.LoadTemplate(nil, viewPDFPaths...)
clientTemplates[templateClientFiles] = filesTmpl
clientTemplates[templateClientProfile] = profileTmpl
@ -257,6 +270,7 @@ func loadClientTemplates(templatesPath string) {
clientTemplates[templateClientShare] = shareTmpl
clientTemplates[templateForgotPassword] = forgotPwdTmpl
clientTemplates[templateResetPassword] = resetPwdTmpl
clientTemplates[templateClientViewPDF] = viewPDFTmpl
}
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)
}
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{
baseClientPage: getBaseClientPageData(pageClientEditFileTitle, webClientEditFilePath, r),
Path: fileName,
Name: path.Base(fileName),
CurrentDir: path.Dir(fileName),
ReadOnly: readOnly,
Data: fileData,
}
@ -427,6 +442,7 @@ func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error stri
Error: error,
CurrentDir: url.QueryEscape(dirName),
DownloadURL: webClientDownloadZipPath,
ViewPDFURL: webClientViewPDFPath,
DirsURL: webClientDirsPath,
CanAddFiles: user.CanAddFilesFromWeb(dirName),
CanCreateDirs: user.CanAddDirsFromWeb(dirName),
@ -650,7 +666,8 @@ func handleClientGetFiles(w http.ResponseWriter, r *http.Request) {
renderFilesPage(w, r, name, "", user)
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 == http.StatusRequestedRangeNotSatisfiable {
renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "")
@ -723,7 +740,7 @@ func handleClientEditFile(w http.ResponseWriter, r *http.Request) {
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) {
@ -1035,3 +1052,19 @@ func handleWebClientPasswordReset(w http.ResponseWriter, r *http.Request) {
}
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)
}

View file

@ -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"
schema:
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:
'200':
description: successful operation

View file

@ -202,6 +202,7 @@
function SearchCursor(doc, query, pos, options) {
this.atOccurrence = false
this.afterEmptyMatch = false
this.doc = doc
pos = pos ? doc.clipPos(pos) : Pos(0, 0)
this.pos = {from: pos, to: pos}
@ -237,21 +238,29 @@
findPrevious: function() {return this.find(true)},
find: function(reverse) {
var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to))
// Implements weird auto-growing behavior on null-matches for
// backwards-compatibility with the vim code (unfortunately)
while (result && CodeMirror.cmpPos(result.from, result.to) == 0) {
var head = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
if (this.afterEmptyMatch && this.atOccurrence) {
// do not return the same 0 width match twice
head = Pos(head.line, head.ch)
if (reverse) {
if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1)
else if (result.from.line == this.doc.firstLine()) result = null
else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1)))
head.ch--;
if (head.ch < 0) {
head.line--;
head.ch = (this.doc.getLine(head.line) || "").length;
}
} else {
if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1)
else if (result.to.line == this.doc.lastLine()) result = null
else result = this.matches(reverse, Pos(result.to.line + 1, 0))
head.ch++;
if (head.ch > (this.doc.getLine(head.line) || "").length) {
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) {
this.pos = result

View file

@ -60,19 +60,13 @@
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-fat-cursor-mark {
background-color: rgba(20, 255, 20, 0.5);
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
}
.cm-animate-fat-cursor {
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;
}
.cm-fat-cursor .CodeMirror-line::selection,
.cm-fat-cursor .CodeMirror-line > span::selection,
.cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; }
.cm-fat-cursor .CodeMirror-line::-moz-selection,
.cm-fat-cursor .CodeMirror-line > span::-moz-selection,
.cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; }
.cm-fat-cursor { caret-color: transparent; }
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }

View file

@ -2351,12 +2351,14 @@
function mapFromLineView(lineView, line, lineN) {
if (lineView.line == line)
{ return {map: lineView.measure.map, cache: lineView.measure.cache} }
for (var i = 0; i < lineView.rest.length; i++)
{ if (lineView.rest[i] == line)
{ return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } }
for (var i$1 = 0; i$1 < lineView.rest.length; i$1++)
{ if (lineNo(lineView.rest[i$1]) > lineN)
{ return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } }
if (lineView.rest) {
for (var i = 0; i < lineView.rest.length; i++)
{ if (lineView.rest[i] == line)
{ return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } }
for (var i$1 = 0; i$1 < lineView.rest.length; i$1++)
{ 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
@ -3150,13 +3152,19 @@
var curFragment = result.cursors = 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++) {
if (!primary && i == doc.sel.primIndex) { continue }
var range = doc.sel.ranges[i];
if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) { continue }
var collapsed = range.empty();
if (collapsed || cm.options.showCursorWhenSelecting)
{ drawSelectionCursor(cm, range.head, curFragment); }
if (customCursor) {
var head = customCursor(cm, range);
if (head) { drawSelectionCursor(cm, head, curFragment); }
} else if (collapsed || cm.options.showCursorWhenSelecting) {
drawSelectionCursor(cm, range.head, curFragment);
}
if (!collapsed)
{ drawSelectionRange(cm, range, selFragment); }
}
@ -3174,9 +3182,8 @@
if (/\bcm-fat-cursor\b/.test(cm.getWrapperElement().className)) {
var charPos = charCoords(cm, head, "div", null, null);
if (charPos.right - charPos.left > 0) {
cursor.style.width = (charPos.right - charPos.left) + "px";
}
var width = charPos.right - charPos.left;
cursor.style.width = (width > 0 ? width : cm.defaultCharWidth()) + "px";
}
if (pos.other) {
@ -3649,6 +3656,7 @@
this.vert.firstChild.style.height =
Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px";
} else {
this.vert.scrollTop = 0;
this.vert.style.display = "";
this.vert.firstChild.style.height = "0";
}
@ -4501,7 +4509,7 @@
function onScrollWheel(cm, e) {
var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y;
var pixelsPerUnit = wheelPixelsPerUnit;
if (event.deltaMode === 0) {
if (e.deltaMode === 0) {
dx = e.deltaX;
dy = e.deltaY;
pixelsPerUnit = 1;
@ -8235,7 +8243,7 @@
}
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;");
// The textarea is kept positioned near the cursor to prevent the
// fact that it'll be scrolled into view on input from scrolling
@ -9832,7 +9840,7 @@
addLegacyProps(CodeMirror);
CodeMirror.version = "5.63.1";
CodeMirror.version = "5.64.0";
return CodeMirror;

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -36,7 +36,9 @@
<span class="font-weight-bold text-primary">Edit file "{{.Path}}"</span>
<span class="btn-toolbar">
<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>
{{end}}
</span>
</h6>
</div>
@ -107,6 +109,9 @@
lineNumbers: true,
styleActiveLine: true,
extraKeys: {"Alt-F": "findPersistent"},
{{if .ReadOnly}}
readOnly: true,
{{end}}
autofocus: true
});
var filename = "{{.Path}}";
@ -126,6 +131,7 @@
});
}
{{if not .ReadOnly}}
function saveFile() {
$('#idSave').addClass("disabled");
cm = document.querySelector('.CodeMirror').CodeMirror;
@ -167,5 +173,6 @@
}
});
}
{{end}}
</script>
{{end}}

View file

@ -8,6 +8,7 @@
<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/dataTables.checkboxes.css" rel="stylesheet">
<link href="{{.StaticURL}}/vendor/lightbox2/css/lightbox.min.css" rel="stylesheet">
<style>
div.dataTables_wrapper span.selected-info,
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/responsive.bootstrap4.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/meta.js"></script>
{{end}}
<script type="text/javascript">
function getIconForFile(filename) {
@ -613,15 +614,37 @@
{ "data": "edit_url",
"render": function (data, type, row) {
if (type === 'display') {
{{if .CanAddFiles}}
var filename = row["name"];
var extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
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 .CanAddFiles}}
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 "";
}

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