implemented attachment downloading
This commit is contained in:
@@ -14,8 +14,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
@@ -277,61 +275,6 @@ func NewTopic(ctxt ui.AmContext) (string, any, error) {
|
||||
return ui.ErrorPage(ctxt, errors.New("invalid button clicked on form"))
|
||||
}
|
||||
|
||||
// slurpFile reads the contrents of a multipart.File into memory.
|
||||
func slurpFile(file *multipart.FileHeader) ([]byte, error) {
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
|
||||
/* AttachmentUpload adds an attachment to a post.
|
||||
* Parameters:
|
||||
* ctxt - The AmContext for the request.
|
||||
* Returns:
|
||||
* Command string dictating what to be rendered.
|
||||
* Data as a parameter for the command string.
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func AttachmentUpload(ctxt ui.AmContext) (string, any, error) {
|
||||
target := ctxt.FormField("tgt")
|
||||
postidStr := ctxt.FormField("post")
|
||||
postId, err := strconv.ParseInt(postidStr, 10, 64)
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, fmt.Errorf("internal error converting postID: %v", err))
|
||||
}
|
||||
if ctxt.FormFieldIsSet("upload") {
|
||||
file, err := ctxt.FormFile("thefile")
|
||||
if err == nil {
|
||||
if file.Size <= (1024 * 1024) { // 1 Mb
|
||||
var post *database.PostHeader
|
||||
post, err = database.AmGetPost(ctxt.Ctx(), postId)
|
||||
if err == nil {
|
||||
var data []byte
|
||||
data, err = slurpFile(file)
|
||||
if err == nil {
|
||||
err = post.SetAttachment(ctxt.Ctx(), ctxt.CurrentUser(), file.Filename, file.Header.Get("Content-Type"), int32(file.Size), data)
|
||||
if err == nil {
|
||||
return "redirect", target, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err = errors.New("the file is too large to be attached")
|
||||
}
|
||||
}
|
||||
|
||||
ctxt.VarMap().Set("target", target)
|
||||
ctxt.VarMap().Set("post", postId)
|
||||
ctxt.VarMap().Set("amsterdam_pageTitle", "Upload Attachment")
|
||||
ctxt.VarMap().Set("errorMessage", err.Error())
|
||||
return "framed_template", "attachment_upload.jet", nil
|
||||
}
|
||||
return ui.ErrorPage(ctxt, errors.New("invalid button clicked on form"))
|
||||
}
|
||||
|
||||
/* breakRange breaks up a post range into two elements.
|
||||
* Parameters:
|
||||
* topic - The topic within which the range is defined.
|
||||
|
||||
@@ -11,12 +11,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
"git.erbosoft.com/amy/amsterdam/ui"
|
||||
)
|
||||
|
||||
// slurpFile reads the contrents of a multipart.File into memory.
|
||||
func slurpFile(file *multipart.FileHeader) ([]byte, error) {
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
|
||||
/* AttachmentUpload adds an attachment to a post.
|
||||
* Parameters:
|
||||
* ctxt - The AmContext for the request.
|
||||
* Returns:
|
||||
* Command string dictating what to be rendered.
|
||||
* Data as a parameter for the command string.
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func AttachmentUpload(ctxt ui.AmContext) (string, any, error) {
|
||||
target := ctxt.FormField("tgt")
|
||||
postidStr := ctxt.FormField("post")
|
||||
postId, err := strconv.ParseInt(postidStr, 10, 64)
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, fmt.Errorf("internal error converting postID: %v", err))
|
||||
}
|
||||
if ctxt.FormFieldIsSet("upload") {
|
||||
file, err := ctxt.FormFile("thefile")
|
||||
if err == nil {
|
||||
var post *database.PostHeader
|
||||
post, err = database.AmGetPost(ctxt.Ctx(), postId)
|
||||
if err == nil {
|
||||
var data []byte
|
||||
data, err = slurpFile(file)
|
||||
if err == nil {
|
||||
err = post.SetAttachment(ctxt.Ctx(), ctxt.CurrentUser(), file.Filename, file.Header.Get("Content-Type"), int32(file.Size), data, ctxt.RemoteIP())
|
||||
if err == nil {
|
||||
return "redirect", target, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctxt.VarMap().Set("target", target)
|
||||
ctxt.VarMap().Set("post", postId)
|
||||
ctxt.VarMap().Set("amsterdam_pageTitle", "Upload Attachment")
|
||||
ctxt.VarMap().Set("errorMessage", err.Error())
|
||||
return "framed_template", "attachment_upload.jet", nil
|
||||
}
|
||||
return ui.ErrorPage(ctxt, errors.New("invalid button clicked on form"))
|
||||
}
|
||||
|
||||
/* AttachmentSend sends the data of an attachment to the browser.
|
||||
* Parameters:
|
||||
* ctxt - The AmContext for the request.
|
||||
* Returns:
|
||||
* Command string dictating what to be rendered.
|
||||
* Data as a parameter for the command string.
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func AttachmentSend(ctxt ui.AmContext) (string, any, error) {
|
||||
postIdStr := ctxt.URLParam("post")
|
||||
postId, err := strconv.ParseInt(postIdStr, 10, 64)
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, fmt.Errorf("internal error converting postID: %v", err))
|
||||
}
|
||||
|
||||
hdr, err := database.AmGetPost(ctxt.Ctx(), postId)
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
|
||||
// Retrieve attachment info and data.
|
||||
info, err := hdr.AttachmentInfo(ctxt.Ctx())
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
} else if info == nil {
|
||||
return ui.ErrorPage(ctxt, errors.New("attachment not found"))
|
||||
}
|
||||
data, err := hdr.AttachmentData(ctxt.Ctx())
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
|
||||
// Record a "hit" on this attachment in the background.
|
||||
ampool.Submit(func(ctx context.Context) {
|
||||
hdr.HitAttachment(ctx)
|
||||
})
|
||||
|
||||
// Send the attachment data.
|
||||
ctxt.SetOutputType(info.MIMEType)
|
||||
if !strings.HasPrefix(info.MIMEType, "image/") {
|
||||
ctxt.SetHeader("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", info.Filename))
|
||||
}
|
||||
return "bytes", data, nil
|
||||
}
|
||||
|
||||
/* HideTopic hides or shows rthe current topic for the current user.
|
||||
* Parameters:
|
||||
* ctxt - The AmContext for the request.
|
||||
|
||||
+64
-1
@@ -82,6 +82,9 @@ func (p *PostHeader) IsPublished(ctx context.Context) (bool, error) {
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func (p *PostHeader) AttachmentInfo(ctx context.Context) (*PostAttachInfo, error) {
|
||||
if p.ScribbleDate != nil && p.ScribbleUid != nil {
|
||||
return nil, errors.New("no attachment data for scribbled post")
|
||||
}
|
||||
rs, err := amdb.QueryContext(ctx, "SELECT filename, mimetype, datalen FROM postattach WHERE postid = ?", p.PostId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -94,6 +97,50 @@ func (p *PostHeader) AttachmentInfo(ctx context.Context) (*PostAttachInfo, error
|
||||
return &rc, err
|
||||
}
|
||||
|
||||
/* AttachmentData returns attachment data for a post.
|
||||
* Parameters:
|
||||
* ctx - Standard Go context value.
|
||||
* Returns:
|
||||
* Attachment data as a byte array.
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func (p *PostHeader) AttachmentData(ctx context.Context) ([]byte, error) {
|
||||
if p.ScribbleDate != nil && p.ScribbleUid != nil {
|
||||
return nil, errors.New("no attachment data for scribbled post")
|
||||
}
|
||||
rs, err := amdb.QueryContext(ctx, "SELECT datalen, stgmethod, data FROM postattach WHERE postid = ?", p.PostId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !rs.Next() {
|
||||
return nil, nil
|
||||
}
|
||||
var datalen int32
|
||||
var stgmethod int16
|
||||
var dbdata []byte
|
||||
err = rs.Scan(&datalen, &stgmethod, &dbdata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if stgmethod == stgMethodPlain {
|
||||
return dbdata, nil
|
||||
}
|
||||
r, err := gzip.NewReader(bytes.NewReader(dbdata))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outdata := make([]byte, 0, datalen)
|
||||
n, err := r.Read(outdata)
|
||||
r.Close()
|
||||
if err != nil || n < int(datalen) {
|
||||
if err == nil {
|
||||
err = errors.New("unable to read entire attachment")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return outdata, nil
|
||||
}
|
||||
|
||||
/* SetAttachment sets the attachment data for a post.
|
||||
* Parameters:
|
||||
* ctx - Standard Go context value.
|
||||
@@ -105,7 +152,11 @@ func (p *PostHeader) AttachmentInfo(ctx context.Context) (*PostAttachInfo, error
|
||||
* Returns:
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func (p *PostHeader) SetAttachment(ctx context.Context, u *User, fileName string, mimeType string, length int32, data []byte) error {
|
||||
func (p *PostHeader) SetAttachment(ctx context.Context, u *User, fileName string, mimeType string, length int32, data []byte, ipaddr string) error {
|
||||
var ar *AuditRecord = nil
|
||||
defer func() {
|
||||
AmStoreAudit(ar)
|
||||
}()
|
||||
if p.ScribbleDate != nil && p.ScribbleUid != nil {
|
||||
return errors.New("cannot attach to scribbled post")
|
||||
}
|
||||
@@ -146,6 +197,18 @@ func (p *PostHeader) SetAttachment(ctx context.Context, u *User, fileName string
|
||||
// Write to the database.
|
||||
_, err = amdb.ExecContext(ctx, "INSERT INTO postattach (postid, datalen, filename, mimetype, stgmethod, data) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
p.PostId, length, fileName, mimeType, stgmethod, realData)
|
||||
// Generate an audit record.
|
||||
ar = AmNewAudit(AuditConferenceUploadAttachment, u.Uid, ipaddr, fmt.Sprintf("post=%d", p.PostId),
|
||||
fmt.Sprintf("len=%d,type=%s,name=%s,method=%d", length, mimeType, fileName, stgmethod))
|
||||
return err
|
||||
}
|
||||
|
||||
// HitAttachment records a "hit" on an attachment.
|
||||
func (p *PostHeader) HitAttachment(ctx context.Context) error {
|
||||
if p.ScribbleDate != nil && p.ScribbleUid != nil {
|
||||
return errors.New("no attachment on scribbled post")
|
||||
}
|
||||
_, err := amdb.ExecContext(ctx, "UPDATE postattach SET hits = hits + 1, lasthit = NOW() WHERE postid = ?", p.PostId)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ func setupEcho() *echo.Echo {
|
||||
e.GET("/create_comm", ui.AmWrap(CreateCommunityForm))
|
||||
e.POST("/create_comm", ui.AmWrap(CreateCommunity))
|
||||
e.POST("/attachment_upload", ui.AmWrap(AttachmentUpload))
|
||||
e.GET("/attachment/:post", ui.AmWrap(AttachmentSend))
|
||||
|
||||
// community group
|
||||
commGroup := e.Group("/comm/:cid", ui.SetCommunity)
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
|
||||
// AmContext is the interface for Amsterdam's wrapper context that exposes the required functionality.
|
||||
type AmContext interface {
|
||||
AddHeader(string, string)
|
||||
ClearCommunityContext()
|
||||
ClearLoginCookie()
|
||||
ClearSession()
|
||||
@@ -59,6 +60,7 @@ type AmContext interface {
|
||||
ReplaceUser(*database.User)
|
||||
SaveSession() error
|
||||
SetCommunityContext(string) error
|
||||
SetHeader(string, string)
|
||||
SetLeftMenu(string)
|
||||
SetLoginCookie(string)
|
||||
SetOutputType(string)
|
||||
@@ -96,6 +98,11 @@ type amContext struct {
|
||||
isMemberLocked bool
|
||||
}
|
||||
|
||||
// AddHeader adds a header to the response.
|
||||
func (c *amContext) AddHeader(key, value string) {
|
||||
c.echoContext.Response().Header().Add(key, value)
|
||||
}
|
||||
|
||||
// ClearCommunityContext clears the community context so changes will be reflected.
|
||||
func (c *amContext) ClearCommunityContext() {
|
||||
c.community = nil
|
||||
@@ -343,6 +350,11 @@ func (c *amContext) SetCommunityContext(param string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetHeader sets a header on the output.
|
||||
func (c *amContext) SetHeader(key, value string) {
|
||||
c.echoContext.Response().Header().Set(key, value)
|
||||
}
|
||||
|
||||
// SetLeftMenu sets the current topmost left menu name value.
|
||||
func (c *amContext) SetLeftMenu(name string) {
|
||||
c.session.Set("leftMenu", name)
|
||||
|
||||
@@ -13,7 +13,9 @@ package ui
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/lctime"
|
||||
"github.com/labstack/echo/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -89,6 +91,9 @@ func ErrorPage(ctxt AmContext, input_err error) (string, any, error) {
|
||||
return "framed_template", "error.jet", nil
|
||||
}
|
||||
|
||||
// expireTime is the expiration time sent in the dynamic headers.
|
||||
var expireTime string = lctime.Strftime("%c", time.Unix(1, 0))
|
||||
|
||||
/* AmWrap wraps the Amsterdam handler function in a wrapper that implements the spec for
|
||||
* Echo handler functions.
|
||||
* Parameters:
|
||||
@@ -99,6 +104,12 @@ func ErrorPage(ctxt AmContext, input_err error) (string, any, error) {
|
||||
func AmWrap(myfunc func(AmContext) (string, any, error)) echo.HandlerFunc {
|
||||
return func(ctxt echo.Context) error {
|
||||
amctxt := AmContextFromEchoContext(ctxt)
|
||||
|
||||
// Add the dynamic headers.
|
||||
ctxt.Response().Header().Set("Pragma", "No-cache")
|
||||
ctxt.Response().Header().Set("Cache-Control", "no-cache")
|
||||
ctxt.Response().Header().Set("Expires", expireTime)
|
||||
|
||||
// Exec the wrapped function.
|
||||
what, rc, err := myfunc(amctxt)
|
||||
if err == nil {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<a href="/user/{{ post_userName }}" target="_blank" class="text-blue-700 hover:text-blue-900">{{ post_userName }}</a>,
|
||||
{{ DisplayDateTime(post_cur.Posted, .) }}</em>)
|
||||
{{ if post_attach.Filename != "" }}
|
||||
<a href="/TODO" title="(Attachment {{ post_attach.Filename }} - {{ post_attach.Length }} bytes)"
|
||||
<a href="/attachment/{{ post_cur.PostId }}" title="(Attachment {{ post_attach.Filename }} - {{ post_attach.Length }} bytes)"
|
||||
{{ if hasPrefix(post_attach.MIMEType, "text/") || hasPrefix(post_attach.MIMEType, "image/" )}}
|
||||
target="_blank"
|
||||
{{ end }}
|
||||
|
||||
Reference in New Issue
Block a user