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) }