mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 04:06:44 +00:00
admin: view images and PDFs inline in the file browser (#9751)
The viewer embedded images and PDFs through the download URL, which sent Content-Disposition: attachment, so the browser downloaded them instead of rendering. Add an inline mode to the download endpoint, limited to images and PDFs so a hostile upload (HTML, SVG) can't run as same-origin script, set X-Content-Type-Options: nosniff, and resolve the MIME the same way the viewer does. The viewer now requests the inline URL.
This commit is contained in:
@@ -9,8 +9,10 @@ import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/operation"
|
||||
@@ -67,7 +69,7 @@ func (h *FileBrowserHandlers) fetchFileContentGrpc(ctx context.Context, filePath
|
||||
// response writer receives the canonical attachment headers and the raw
|
||||
// bytes; this replaces the HTTP-to-filer proxy that used to run in
|
||||
// DownloadFile.
|
||||
func (h *FileBrowserHandlers) downloadFileGrpc(ctx context.Context, filePath string, w http.ResponseWriter) error {
|
||||
func (h *FileBrowserHandlers) downloadFileGrpc(ctx context.Context, filePath string, w http.ResponseWriter, inline bool) error {
|
||||
cleanFilePath, err := h.validateAndCleanFilePath(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -84,16 +86,19 @@ func (h *FileBrowserHandlers) downloadFileGrpc(ctx context.Context, filePath str
|
||||
size := int64(filer.FileSize(entry))
|
||||
|
||||
fileName := path.Base(cleanFilePath)
|
||||
w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": fileName}))
|
||||
|
||||
contentType := ""
|
||||
if entry.Attributes != nil {
|
||||
contentType = entry.Attributes.Mime
|
||||
}
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
// Resolve mime like the viewer does so Content-Type and the inline check agree.
|
||||
contentType := dash.ResolveEntryMime(entry)
|
||||
|
||||
// Only inline images and PDFs; serve the rest as attachments so a hostile upload
|
||||
// (HTML, SVG) can't run as same-origin script. nosniff locks the declared type.
|
||||
disposition := "attachment"
|
||||
if inline && (strings.HasPrefix(contentType, "image/") || contentType == "application/pdf") {
|
||||
disposition = "inline"
|
||||
}
|
||||
w.Header().Set("Content-Disposition", mime.FormatMediaType(disposition, map[string]string{"filename": fileName}))
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
|
||||
@@ -397,8 +397,9 @@ func (h *FileBrowserHandlers) DownloadFile(w http.ResponseWriter, r *http.Reques
|
||||
writeJSONError(w, http.StatusBadRequest, "File path is required")
|
||||
return
|
||||
}
|
||||
inline := r.URL.Query().Get("inline") == "true"
|
||||
tracker := &responseWriteTracker{ResponseWriter: w}
|
||||
if err := h.downloadFileGrpc(r.Context(), filePath, tracker); err != nil {
|
||||
if err := h.downloadFileGrpc(r.Context(), filePath, tracker, inline); err != nil {
|
||||
// Once bytes have been written we can't switch to a JSON error body
|
||||
// without corrupting the partial response — log and stop. Before any
|
||||
// write the response is still uncommitted, so a 502 with details is
|
||||
|
||||
@@ -1788,7 +1788,7 @@ function createFileViewerContent(file, content) {
|
||||
if (file.mime.startsWith('image/')) {
|
||||
return `
|
||||
<div class="text-center">
|
||||
<img src="${basePath('/api/files/download?path=' + encodeURIComponent(file.full_path))}"
|
||||
<img src="${basePath('/api/files/download?path=' + encodeURIComponent(file.full_path) + '&inline=true')}"
|
||||
class="img-fluid" alt="${file.name}" style="max-height: 500px;">
|
||||
</div>
|
||||
`;
|
||||
@@ -1806,7 +1806,7 @@ function createFileViewerContent(file, content) {
|
||||
} else if (file.mime === 'application/pdf') {
|
||||
return `
|
||||
<div class="text-center">
|
||||
<embed src="${basePath('/api/files/download?path=' + encodeURIComponent(file.full_path))}"
|
||||
<embed src="${basePath('/api/files/download?path=' + encodeURIComponent(file.full_path) + '&inline=true')}"
|
||||
type="application/pdf" width="100%" height="500px">
|
||||
</div>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user