Files
files/internal/handler/node.go
Grendgi 6bd2251a98
All checks were successful
CI / hygiene (push) Successful in 2s
Build and Deploy / build-and-deploy (push) Successful in 31s
CI / test (push) Successful in 19s
fix: render public folder links
2026-06-16 16:39:04 +03:00

723 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"crypto/rand"
"encoding/base64"
"errors"
"html"
"io"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
commonmw "gitea.estateliga.work/admin/portal-common/middleware"
"github.com/go-chi/chi/v5"
"files-service/internal/config"
"files-service/internal/model"
"files-service/internal/repository"
"files-service/internal/storage"
)
type NodeHandler struct {
cfg *config.Config
repo *repository.NodeRepository
store *storage.Storage
}
func NewNodeHandler(cfg *config.Config, repo *repository.NodeRepository, store *storage.Storage) *NodeHandler {
return &NodeHandler{cfg: cfg, repo: repo, store: store}
}
func (h *NodeHandler) List(w http.ResponseWriter, r *http.Request) {
userID := commonmw.GetUserID(r.Context())
scope := r.URL.Query().Get("scope")
parentID := emptyToNil(r.URL.Query().Get("parent_id"))
nodes, err := h.repo.List(r.Context(), userID, subordinates(r), scope, parentID)
if err != nil {
writeInternalError(w, r, err, "failed to list files")
return
}
writeJSON(w, http.StatusOK, nodes)
}
func (h *NodeHandler) Get(w http.ResponseWriter, r *http.Request) {
node, ok := h.requireNode(w, r)
if !ok {
return
}
writeJSON(w, http.StatusOK, node)
}
func (h *NodeHandler) CreateFolder(w http.ResponseWriter, r *http.Request) {
var req model.CreateFolderRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json")
return
}
req.Title = strings.TrimSpace(req.Title)
if req.Title == "" {
writeError(w, http.StatusBadRequest, "title is required")
return
}
userID := commonmw.GetUserID(r.Context())
if !h.requireWritableParent(w, r, userID, req.ParentID) {
return
}
node, err := h.repo.CreateFolder(r.Context(), req.Title, req.ParentID, userID)
if err != nil {
writeInternalError(w, r, err, "failed to create folder")
return
}
h.repo.Audit(r.Context(), userID, "files.folder_create", "files_node", node.ID, "{}")
writeJSON(w, http.StatusCreated, node)
}
func (h *NodeHandler) ListOfficeLinks(w http.ResponseWriter, r *http.Request) {
userID := commonmw.GetUserID(r.Context())
links, err := h.repo.ListOfficeExternalURLs(r.Context(), userID, subordinates(r))
if err != nil {
writeInternalError(w, r, err, "failed to list office links")
return
}
writeJSON(w, http.StatusOK, links)
}
func (h *NodeHandler) CreateOfficeDocument(w http.ResponseWriter, r *http.Request) {
var req model.CreateOfficeDocumentRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json")
return
}
req.Title = strings.TrimSpace(req.Title)
req.OfficeID = strings.TrimSpace(req.OfficeID)
req.OfficeFormat = strings.Trim(strings.ToLower(req.OfficeFormat), ". ")
if req.Title == "" {
writeError(w, http.StatusBadRequest, "title is required")
return
}
if req.OfficeID == "" {
writeError(w, http.StatusBadRequest, "office_id is required")
return
}
if !allowedOfficeFormat(req.OfficeFormat) {
writeError(w, http.StatusBadRequest, "office_format is not allowed")
return
}
userID := commonmw.GetUserID(r.Context())
if !h.requireWritableParent(w, r, userID, req.ParentID) {
return
}
externalURL := "/office/" + req.OfficeID
originalFilename := req.Title + "." + req.OfficeFormat
mimeType := req.OfficeFormat
node, err := h.repo.CreateOfficeDocument(r.Context(), &model.Node{
ParentID: req.ParentID,
Title: req.Title,
OwnerUserID: userID,
OriginalFilename: &originalFilename,
MimeType: &mimeType,
Extension: &req.OfficeFormat,
SizeBytes: req.SizeBytes,
OfficeFormat: &req.OfficeFormat,
ExternalURL: &externalURL,
})
if err != nil {
writeInternalError(w, r, err, "failed to create office document")
return
}
h.repo.Audit(r.Context(), userID, "files.office_document_create", "files_node", node.ID, "{}")
writeJSON(w, http.StatusCreated, node)
}
func (h *NodeHandler) UploadFile(w http.ResponseWriter, r *http.Request) {
if !h.store.Configured() {
writeError(w, http.StatusServiceUnavailable, "storage not configured")
return
}
if err := r.ParseMultipartForm(64 << 20); err != nil {
writeError(w, http.StatusBadRequest, "invalid multipart")
return
}
file, header, err := r.FormFile("file")
if err != nil {
writeError(w, http.StatusBadRequest, "file is required")
return
}
defer file.Close()
filename := filepath.Base(header.Filename)
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), "."))
if !storage.AllowedExtension(ext) {
writeError(w, http.StatusBadRequest, "file type is not allowed")
return
}
title := strings.TrimSpace(r.FormValue("title"))
if title == "" {
title = strings.TrimSuffix(filename, filepath.Ext(filename))
}
userID := commonmw.GetUserID(r.Context())
parentID := emptyToNil(r.FormValue("parent_id"))
if !h.requireWritableParent(w, r, userID, parentID) {
return
}
key := storage.GenerateKey(userID, filename)
contentType := storage.GuessContentType(filename, header.Header.Get("Content-Type"))
if err := h.store.PutObject(r.Context(), key, file, header.Size, contentType); err != nil {
writeInternalError(w, r, err, "failed to upload file")
return
}
node, err := h.repo.CreateFile(r.Context(), &model.Node{
ParentID: parentID,
Title: title,
OwnerUserID: userID,
StorageKey: &key,
OriginalFilename: &filename,
MimeType: &contentType,
Extension: &ext,
SizeBytes: header.Size,
})
if err != nil {
writeInternalError(w, r, err, "failed to create file")
return
}
h.repo.Audit(r.Context(), userID, "files.file_upload", "files_node", node.ID, "{}")
writeJSON(w, http.StatusCreated, node)
}
func (h *NodeHandler) Update(w http.ResponseWriter, r *http.Request) {
var req model.UpdateNodeRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json")
return
}
if req.Title != nil {
title := strings.TrimSpace(*req.Title)
if title == "" {
writeError(w, http.StatusBadRequest, "title is required")
return
}
req.Title = &title
}
userID := commonmw.GetUserID(r.Context())
node, err := h.repo.Update(r.Context(), chi.URLParam(r, "id"), userID, req)
if errors.Is(err, repository.ErrNotFound) {
writeError(w, http.StatusNotFound, "file not found")
return
}
if err != nil {
writeInternalError(w, r, err, "failed to update file")
return
}
h.repo.Audit(r.Context(), userID, "files.node_update", "files_node", node.ID, "{}")
writeJSON(w, http.StatusOK, node)
}
func (h *NodeHandler) Move(w http.ResponseWriter, r *http.Request) {
var req model.MoveNodeRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json")
return
}
userID := commonmw.GetUserID(r.Context())
if !h.requireWritableParent(w, r, userID, req.ParentID) {
return
}
node, err := h.repo.Move(r.Context(), chi.URLParam(r, "id"), userID, subordinates(r), req.ParentID)
if errors.Is(err, repository.ErrNotFound) {
writeError(w, http.StatusNotFound, "file not found")
return
}
if errors.Is(err, model.ErrInvalidMove) {
writeError(w, http.StatusBadRequest, "folder cannot be moved inside itself")
return
}
if err != nil {
writeInternalError(w, r, err, "failed to move file")
return
}
h.repo.Audit(r.Context(), userID, "files.node_move", "files_node", node.ID, "{}")
writeJSON(w, http.StatusOK, node)
}
func (h *NodeHandler) Restore(w http.ResponseWriter, r *http.Request) {
userID := commonmw.GetUserID(r.Context())
node, err := h.repo.Restore(r.Context(), chi.URLParam(r, "id"), userID, subordinates(r))
if errors.Is(err, repository.ErrNotFound) {
writeError(w, http.StatusNotFound, "file not found")
return
}
if err != nil {
writeInternalError(w, r, err, "failed to restore file")
return
}
h.repo.Audit(r.Context(), userID, "files.node_restore", "files_node", node.ID, "{}")
writeJSON(w, http.StatusOK, node)
}
func (h *NodeHandler) Delete(w http.ResponseWriter, r *http.Request) {
userID := commonmw.GetUserID(r.Context())
id := chi.URLParam(r, "id")
purgeAfter := time.Now().Add(30 * 24 * time.Hour)
if err := h.repo.SoftDelete(r.Context(), id, userID, purgeAfter); errors.Is(err, repository.ErrNotFound) {
writeError(w, http.StatusNotFound, "file not found")
return
} else if err != nil {
writeInternalError(w, r, err, "failed to delete file")
return
}
h.repo.Audit(r.Context(), userID, "files.node_delete", "files_node", id, "{}")
w.WriteHeader(http.StatusNoContent)
}
func (h *NodeHandler) Download(w http.ResponseWriter, r *http.Request) {
node, ok := h.requireNode(w, r)
if !ok {
return
}
h.streamNode(w, r, node)
}
func (h *NodeHandler) ListAccess(w http.ResponseWriter, r *http.Request) {
node, ok := h.requireNode(w, r)
if !ok {
return
}
if node.EffectiveAccess != model.AccessEdit {
writeError(w, http.StatusForbidden, "edit access required")
return
}
access, err := h.repo.ListAccess(r.Context(), node.ID)
if err != nil {
writeInternalError(w, r, err, "failed to list access")
return
}
writeJSON(w, http.StatusOK, access)
}
func (h *NodeHandler) ReplaceAccess(w http.ResponseWriter, r *http.Request) {
var req model.ReplaceAccessRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json")
return
}
userID := commonmw.GetUserID(r.Context())
id := chi.URLParam(r, "id")
if err := h.repo.ReplaceAccess(r.Context(), id, userID, req.Access); errors.Is(err, repository.ErrNotFound) {
writeError(w, http.StatusNotFound, "file not found")
return
} else if err != nil {
writeInternalError(w, r, err, "failed to update access")
return
}
h.repo.Audit(r.Context(), userID, "files.access_update", "files_node", id, "{}")
w.WriteHeader(http.StatusNoContent)
}
func (h *NodeHandler) ListPublicLinks(w http.ResponseWriter, r *http.Request) {
userID := commonmw.GetUserID(r.Context())
id := chi.URLParam(r, "id")
links, err := h.repo.ListPublicLinks(r.Context(), id, userID)
if err != nil {
writeInternalError(w, r, err, "failed to list public links")
return
}
for i := range links {
if links[i].URL != "" {
links[i].URL = h.publicURL(links[i].URL)
}
}
writeJSON(w, http.StatusOK, links)
}
func (h *NodeHandler) CreatePublicLink(w http.ResponseWriter, r *http.Request) {
var req model.PublicLinkRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json")
return
}
if req.ExpiresAt.Before(time.Now().Add(time.Minute)) {
writeError(w, http.StatusBadRequest, "expires_at must be in future")
return
}
token, err := newToken()
if err != nil {
writeInternalError(w, r, err, "failed to create public link")
return
}
userID := commonmw.GetUserID(r.Context())
id := chi.URLParam(r, "id")
link, err := h.repo.CreatePublicLink(r.Context(), id, userID, token, req.ExpiresAt)
if errors.Is(err, repository.ErrNotFound) {
writeError(w, http.StatusNotFound, "file not found")
return
}
if err != nil {
writeInternalError(w, r, err, "failed to create public link")
return
}
h.repo.Audit(r.Context(), userID, "files.public_link_create", "files_node", id, "{}")
link.URL = h.publicURL(token)
writeJSON(w, http.StatusCreated, link)
}
func (h *NodeHandler) PublicMeta(w http.ResponseWriter, r *http.Request) {
node, ok := h.publicNode(w, r)
if !ok {
return
}
if node.NodeType == model.NodeTypeFile {
h.renderPublicPreview(w, node, h.publicURL(chi.URLParam(r, "token"))+"/download")
return
}
if node.NodeType == model.NodeTypeFolder {
children, err := h.repo.ListChildrenForPublic(r.Context(), node.ID)
if err != nil {
writeInternalError(w, r, err, "failed to list folder")
return
}
h.renderPublicFolder(w, r, node, children)
return
}
h.renderPublicUnavailable(w, node)
}
func (h *NodeHandler) PublicDownload(w http.ResponseWriter, r *http.Request) {
node, ok := h.publicNode(w, r)
if !ok {
return
}
h.streamNode(w, r, node)
}
func (h *NodeHandler) PublicChildMeta(w http.ResponseWriter, r *http.Request) {
node, ok := h.publicNodeByID(w, r)
if !ok {
return
}
token := chi.URLParam(r, "token")
if node.NodeType == model.NodeTypeFile {
h.renderPublicPreview(w, node, h.publicNodeURL(token, node.ID)+"/download")
return
}
if node.NodeType == model.NodeTypeFolder {
children, err := h.repo.ListChildrenForPublic(r.Context(), node.ID)
if err != nil {
writeInternalError(w, r, err, "failed to list folder")
return
}
h.renderPublicFolder(w, r, node, children)
return
}
h.renderPublicUnavailable(w, node)
}
func (h *NodeHandler) PublicChildDownload(w http.ResponseWriter, r *http.Request) {
node, ok := h.publicNodeByID(w, r)
if !ok {
return
}
h.streamNode(w, r, node)
}
func (h *NodeHandler) requireNode(w http.ResponseWriter, r *http.Request) (*model.Node, bool) {
node, err := h.repo.GetForUser(r.Context(), chi.URLParam(r, "id"), commonmw.GetUserID(r.Context()), subordinates(r))
if errors.Is(err, repository.ErrNotFound) {
writeError(w, http.StatusNotFound, "file not found")
return nil, false
}
if err != nil {
writeInternalError(w, r, err, "failed to get file")
return nil, false
}
return node, true
}
func (h *NodeHandler) requireWritableParent(w http.ResponseWriter, r *http.Request, userID string, parentID *string) bool {
if parentID == nil || strings.TrimSpace(*parentID) == "" {
return true
}
parent, err := h.repo.GetForUser(r.Context(), *parentID, userID, subordinates(r))
if errors.Is(err, repository.ErrNotFound) {
writeError(w, http.StatusNotFound, "parent folder not found")
return false
}
if err != nil {
writeInternalError(w, r, err, "failed to check parent folder")
return false
}
if parent.NodeType != model.NodeTypeFolder {
writeError(w, http.StatusBadRequest, "parent_id must point to folder")
return false
}
if parent.EffectiveAccess != model.AccessEdit {
writeError(w, http.StatusForbidden, "edit access to parent folder required")
return false
}
return true
}
func (h *NodeHandler) publicNode(w http.ResponseWriter, r *http.Request) (*model.Node, bool) {
node, err := h.repo.GetByPublicToken(r.Context(), chi.URLParam(r, "token"))
if errors.Is(err, repository.ErrNotFound) {
writeError(w, http.StatusNotFound, "public link not found")
return nil, false
}
if err != nil {
writeInternalError(w, r, err, "failed to open public link")
return nil, false
}
return node, true
}
func (h *NodeHandler) publicNodeByID(w http.ResponseWriter, r *http.Request) (*model.Node, bool) {
node, err := h.repo.GetPublicDescendant(r.Context(), chi.URLParam(r, "token"), chi.URLParam(r, "id"))
if errors.Is(err, repository.ErrNotFound) {
writeError(w, http.StatusNotFound, "public file not found")
return nil, false
}
if err != nil {
writeInternalError(w, r, err, "failed to open public file")
return nil, false
}
return node, true
}
func (h *NodeHandler) renderPublicPreview(w http.ResponseWriter, node *model.Node, downloadURL string) {
title := node.Title
if node.OriginalFilename != nil && *node.OriginalFilename != "" {
title = *node.OriginalFilename
}
mimeType := ""
if node.MimeType != nil {
mimeType = strings.ToLower(*node.MimeType)
}
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) renderPublicFolder(w http.ResponseWriter, r *http.Request, node *model.Node, children []model.Node) {
token := chi.URLParam(r, "token")
items := `<div class="empty">В папке пока нет файлов.</div>`
if len(children) > 0 {
var b strings.Builder
b.WriteString(`<div class="list">`)
for _, child := range children {
title := child.Title
if child.OriginalFilename != nil && *child.OriginalFilename != "" {
title = *child.OriginalFilename
}
kind := publicKind(child)
href := h.publicNodeURL(token, child.ID)
b.WriteString(`<a class="item" href="` + html.EscapeString(href) + `">`)
b.WriteString(`<span class="badge">` + html.EscapeString(kind) + `</span>`)
b.WriteString(`<span class="title">` + html.EscapeString(title) + `</span>`)
if child.NodeType != model.NodeTypeFolder {
b.WriteString(`<span class="size">` + html.EscapeString(formatBytes(child.SizeBytes)) + `</span>`)
}
b.WriteString(`</a>`)
}
b.WriteString(`</div>`)
items = b.String()
}
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(node.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: 22px; line-height: 1.35; font-weight: 800; }
.meta { margin-top: 6px; color: #9aa7bd; font-size: 14px; }
main { padding: 24px; }
.list { display: grid; gap: 10px; max-width: 980px; margin: 0 auto; }
.item { display: grid; grid-template-columns: 72px minmax(0, 1fr) auto; gap: 14px; align-items: center; min-height: 58px; padding: 12px 14px; border: 1px solid #263044; border-radius: 10px; background: #151a2a; color: inherit; text-decoration: none; }
.item:hover { border-color: #6366f1; background: #191f34; }
.badge { display: grid; place-items: center; min-height: 34px; border-radius: 8px; background: #232844; color: #c7d2fe; font-size: 12px; font-weight: 800; }
.title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 700; }
.size { color: #9aa7bd; font-size: 14px; }
.empty { max-width: 640px; margin: 0 auto; padding: 28px; border: 1px dashed #263044; border-radius: 10px; background: #151a2a; color: #c6d0e1; text-align: center; }
@media (max-width: 680px) { .item { grid-template-columns: 58px minmax(0, 1fr); } .size { grid-column: 2; } }
</style>
</head>
<body>
<div class="shell">
<header>
<h1>`+html.EscapeString(node.Title)+`</h1>
<div class="meta">Публичный просмотр папки</div>
</header>
<main>`+items+`</main>
</div>
</body>
</html>`)
}
func (h *NodeHandler) renderPublicUnavailable(w http.ResponseWriter, node *model.Node) {
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(node.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;display:grid;place-items:center;padding:24px}.card{width:min(640px,100%);padding:28px;border:1px solid #263044;border-radius:10px;background:#151a2a}.muted{color:#9aa7bd}</style></head>
<body><main class="card"><h1>`+html.EscapeString(node.Title)+`</h1><p class="muted">Публичный предпросмотр для этого типа документа пока недоступен.</p></main></body></html>`)
}
func (h *NodeHandler) publicURL(token string) string {
return strings.TrimRight(h.cfg.PublicBaseURL, "/") + "/api/files/public/" + token
}
func (h *NodeHandler) publicNodeURL(token, nodeID string) string {
return h.publicURL(token) + "/nodes/" + nodeID
}
func publicKind(node model.Node) string {
switch node.NodeType {
case model.NodeTypeFolder:
return "DIR"
case model.NodeTypeOfficeDocument:
if node.OfficeFormat != nil && *node.OfficeFormat != "" {
return strings.ToUpper(*node.OfficeFormat)
}
return "DOC"
case model.NodeTypeFile:
if node.Extension != nil && *node.Extension != "" {
return strings.ToUpper(*node.Extension)
}
return "FILE"
default:
return "FILE"
}
}
func formatBytes(bytes int64) string {
if bytes <= 0 {
return "0 Б"
}
units := []string{"Б", "КБ", "МБ", "ГБ"}
size := float64(bytes)
idx := 0
for size >= 1024 && idx < len(units)-1 {
size /= 1024
idx++
}
if size >= 10 || idx == 0 {
return strings.TrimSuffix(strings.TrimSuffix(formatFloat(size, 0), ".0"), ".") + " " + units[idx]
}
return strings.TrimSuffix(strings.TrimSuffix(formatFloat(size, 1), ".0"), ".") + " " + units[idx]
}
func formatFloat(v float64, precision int) string {
if precision == 0 {
return strconv.FormatInt(int64(v+0.5), 10)
}
return strconv.FormatFloat(v, 'f', precision, 64)
}
func (h *NodeHandler) streamNode(w http.ResponseWriter, r *http.Request, node *model.Node) {
if node.NodeType == model.NodeTypeFolder || node.StorageKey == nil {
writeError(w, http.StatusBadRequest, "node is not downloadable")
return
}
info, err := h.store.Stat(r.Context(), *node.StorageKey)
if err != nil {
writeInternalError(w, r, err, "failed to open file")
return
}
start, end, hasRange := storage.ParseRange(r.Header.Get("Range"), info.Size)
body, info, err := h.store.GetObject(r.Context(), *node.StorageKey, start, end)
if err != nil {
writeInternalError(w, r, err, "failed to stream file")
return
}
defer body.Close()
filename := node.Title
if node.OriginalFilename != nil && *node.OriginalFilename != "" {
filename = *node.OriginalFilename
}
w.Header().Set("Content-Disposition", `inline; filename="`+strings.ReplaceAll(filename, `"`, "")+`"`)
storage.WriteRangeResponse(w, info.ContentType, info.Size, start, end, hasRange)
_, _ = io.Copy(w, body)
}
func emptyToNil(v string) *string {
v = strings.TrimSpace(v)
if v == "" {
return nil
}
return &v
}
func allowedOfficeFormat(format string) bool {
switch format {
case "doc", "docx", "odt", "xls", "xlsx", "xlsm", "ods", "ppt", "pptx", "odp":
return true
default:
return false
}
}
func subordinates(r *http.Request) []string {
ids := csvHeader(r, "X-User-Subordinates")
if len(ids) == 0 {
ids = csvHeader(r, "X-User-Subordinate-Ids")
}
return ids
}
func newToken() (string, error) {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(buf), nil
}