fix: render public file previews
All checks were successful
CI / hygiene (push) Successful in 1s
Build and Deploy / build-and-deploy (push) Successful in 29s
CI / test (push) Successful in 20s

This commit is contained in:
Grendgi
2026-06-16 16:12:03 +03:00
parent c831d2c7c6
commit 44ea1fa36b

View File

@@ -4,6 +4,7 @@ import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"errors" "errors"
"html"
"io" "io"
"net/http" "net/http"
"path/filepath" "path/filepath"
@@ -343,7 +344,7 @@ func (h *NodeHandler) CreatePublicLink(w http.ResponseWriter, r *http.Request) {
h.repo.Audit(r.Context(), userID, "files.public_link_create", "files_node", id, "{}") h.repo.Audit(r.Context(), userID, "files.public_link_create", "files_node", id, "{}")
writeJSON(w, http.StatusCreated, model.PublicLinkResponse{ writeJSON(w, http.StatusCreated, model.PublicLinkResponse{
ID: linkID, ID: linkID,
URL: strings.TrimRight(h.cfg.PublicBaseURL, "/") + "/api/files/public/" + token, URL: h.publicURL(token),
ExpiresAt: req.ExpiresAt, ExpiresAt: req.ExpiresAt,
}) })
} }
@@ -353,6 +354,10 @@ func (h *NodeHandler) PublicMeta(w http.ResponseWriter, r *http.Request) {
if !ok { if !ok {
return return
} }
if node.NodeType == model.NodeTypeFile {
h.renderPublicPreview(w, r, node)
return
}
response := model.PublicNodeResponse{Node: node} response := model.PublicNodeResponse{Node: node}
if node.NodeType == model.NodeTypeFolder { if node.NodeType == model.NodeTypeFolder {
children, err := h.repo.ListChildrenForPublic(r.Context(), node.ID) children, err := h.repo.ListChildrenForPublic(r.Context(), node.ID)
@@ -423,6 +428,65 @@ func (h *NodeHandler) publicNode(w http.ResponseWriter, r *http.Request) (*model
return node, true return node, true
} }
func (h *NodeHandler) renderPublicPreview(w http.ResponseWriter, r *http.Request, node *model.Node) {
title := node.Title
if node.OriginalFilename != nil && *node.OriginalFilename != "" {
title = *node.OriginalFilename
}
mimeType := ""
if node.MimeType != nil {
mimeType = strings.ToLower(*node.MimeType)
}
downloadURL := h.publicURL(chi.URLParam(r, "token")) + "/download"
preview := `<div class="empty">Предпросмотр для этого типа файла недоступен.</div>`
switch {
case strings.HasPrefix(mimeType, "image/"):
preview = `<img class="preview-media" src="` + html.EscapeString(downloadURL) + `" alt="">`
case mimeType == "application/pdf":
preview = `<iframe class="preview-frame" src="` + html.EscapeString(downloadURL) + `"></iframe>`
case strings.HasPrefix(mimeType, "video/"):
preview = `<video class="preview-media" src="` + html.EscapeString(downloadURL) + `" controls></video>`
case strings.HasPrefix(mimeType, "audio/"):
preview = `<audio class="preview-audio" src="` + html.EscapeString(downloadURL) + `" controls></audio>`
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, `<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>`+html.EscapeString(title)+`</title>
<style>
:root { color-scheme: dark; font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #0d111b; color: #edf2ff; }
body { margin: 0; min-height: 100vh; background: #0d111b; }
.shell { min-height: 100vh; display: grid; grid-template-rows: auto 1fr; }
header { padding: 18px 24px; border-bottom: 1px solid #263044; background: #151a2a; }
h1 { margin: 0; font-size: 20px; line-height: 1.35; font-weight: 700; }
.meta { margin-top: 6px; color: #9aa7bd; font-size: 14px; }
main { padding: 24px; display: grid; place-items: center; overflow: auto; }
.preview-media { max-width: 100%; max-height: calc(100vh - 132px); border-radius: 10px; object-fit: contain; background: #080b12; }
.preview-frame { width: min(1200px, 100%); height: calc(100vh - 132px); border: 1px solid #263044; border-radius: 10px; background: #080b12; }
.preview-audio { width: min(720px, 100%); }
.empty { width: min(640px, 100%); padding: 28px; border: 1px solid #263044; border-radius: 10px; background: #151a2a; color: #c6d0e1; text-align: center; }
</style>
</head>
<body>
<div class="shell">
<header>
<h1>`+html.EscapeString(title)+`</h1>
<div class="meta">Публичный просмотр файла</div>
</header>
<main>`+preview+`</main>
</div>
</body>
</html>`)
}
func (h *NodeHandler) publicURL(token string) string {
return strings.TrimRight(h.cfg.PublicBaseURL, "/") + "/api/files/public/" + token
}
func (h *NodeHandler) streamNode(w http.ResponseWriter, r *http.Request, node *model.Node) { func (h *NodeHandler) streamNode(w http.ResponseWriter, r *http.Request, node *model.Node) {
if node.NodeType == model.NodeTypeFolder || node.StorageKey == nil { if node.NodeType == model.NodeTypeFolder || node.StorageKey == nil {
writeError(w, http.StatusBadRequest, "node is not downloadable") writeError(w, http.StatusBadRequest, "node is not downloadable")