teg/backend/chat_attachments.go
2026-06-13 10:15:57 +02:00

161 lines
3.7 KiB
Go

package main
import (
"fmt"
"io"
"mime"
"net/http"
"path/filepath"
"strings"
)
const maxChatAttachmentSize = 10 << 20
func (s *Server) handleUploadChatAttachment(w http.ResponseWriter, r *http.Request) {
user, ok := userFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
r.Body = http.MaxBytesReader(w, r.Body, maxChatAttachmentSize+(1<<20))
if err := r.ParseMultipartForm(maxChatAttachmentSize); err != nil {
writeError(w, http.StatusBadRequest, "Datei ist zu groß oder ungültig")
return
}
file, header, err := r.FormFile("file")
if err != nil {
writeError(w, http.StatusBadRequest, "Datei fehlt")
return
}
defer file.Close()
data, err := io.ReadAll(io.LimitReader(file, maxChatAttachmentSize+1))
if err != nil {
writeError(w, http.StatusBadRequest, "Datei konnte nicht gelesen werden")
return
}
if len(data) == 0 {
writeError(w, http.StatusBadRequest, "Datei ist leer")
return
}
if len(data) > maxChatAttachmentSize {
writeError(w, http.StatusBadRequest, "Datei darf höchstens 10 MB groß sein")
return
}
fileName := strings.TrimSpace(filepath.Base(header.Filename))
if fileName == "" || fileName == "." {
fileName = "Anhang"
}
contentType := strings.TrimSpace(header.Header.Get("Content-Type"))
if contentType == "" {
contentType = http.DetectContentType(data)
}
var attachment ChatAttachment
err = s.db.QueryRow(
r.Context(),
`
INSERT INTO message_attachments (
uploader_id,
file_name,
content_type,
size_bytes,
data
)
VALUES ($1, $2, $3, $4, $5)
RETURNING id::TEXT, file_name, content_type, size_bytes
`,
user.ID,
fileName,
contentType,
len(data),
data,
).Scan(
&attachment.ID,
&attachment.FileName,
&attachment.ContentType,
&attachment.SizeBytes,
)
if err != nil {
writeError(w, http.StatusInternalServerError, "Datei konnte nicht gespeichert werden")
return
}
attachment.URL = "/chat/attachments/" + attachment.ID
writeJSON(w, http.StatusCreated, map[string]any{"attachment": attachment})
}
func (s *Server) handleDownloadChatAttachment(w http.ResponseWriter, r *http.Request) {
user, ok := userFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "Unauthorized")
return
}
attachmentID := strings.TrimSpace(r.PathValue("id"))
var uploaderID string
var messageID string
var conversationID string
var fileName string
var contentType string
var data []byte
err := s.db.QueryRow(
r.Context(),
`
SELECT
a.uploader_id::TEXT,
COALESCE(a.message_id::TEXT, ''),
COALESCE(m.conversation_id::TEXT, ''),
a.file_name,
a.content_type,
a.data
FROM message_attachments a
LEFT JOIN messages m ON m.id = a.message_id
WHERE a.id = $1
`,
attachmentID,
).Scan(
&uploaderID,
&messageID,
&conversationID,
&fileName,
&contentType,
&data,
)
if err != nil {
writeError(w, http.StatusNotFound, "Datei wurde nicht gefunden")
return
}
allowed := messageID == "" && uploaderID == user.ID
if messageID != "" {
allowed = s.canAccessConversation(r.Context(), conversationID, user.ID)
}
if !allowed {
writeError(w, http.StatusForbidden, "Kein Zugriff auf diese Datei")
return
}
if contentType == "" {
contentType = "application/octet-stream"
}
dispositionType := "attachment"
if strings.HasPrefix(strings.ToLower(contentType), "image/") {
dispositionType = "inline"
}
disposition := mime.FormatMediaType(dispositionType, map[string]string{
"filename": fileName,
})
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Disposition", disposition)
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data)))
w.WriteHeader(http.StatusOK)
_, _ = w.Write(data)
}