From 43bd810942a7140300d34e046f6a2c49355cd92e Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Mon, 17 Nov 2025 21:50:53 -0700 Subject: [PATCH] wrote the code to handle attachment upload (and link from new-topic post) --- conference.go | 79 +++++++++++++++++++++++++++++++++- database/post.go | 51 ++++++++++++++++++++++ database/topic.go | 46 +++++++++++++++----- main.go | 1 + ui/views/attachment_upload.jet | 7 +-- 5 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 database/post.go diff --git a/conference.go b/conference.go index a45513f..8d0f69b 100644 --- a/conference.go +++ b/conference.go @@ -13,7 +13,10 @@ package main import ( "errors" "fmt" + "io" + "mime/multipart" "net/http" + "strconv" "git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/htmlcheck" @@ -48,6 +51,7 @@ func conferencesPrequel(ctxt ui.AmContext) (string, any, error) { return "", nil, nil } +// singleConferencePrequel consolidates some of the basic conference checks into one function. func singleConferencePrequel(ctxt ui.AmContext) (string, any, error) { cmd, arg, err := conferencesPrequel(ctxt) if cmd != "" { @@ -210,6 +214,14 @@ func NewTopicForm(ctxt ui.AmContext) (string, any, error) { return "framed_template", "new_topic.jet", nil } +/* NewTopic creates a new topic and posts the initial message. + * 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 NewTopic(ctxt ui.AmContext) (string, any, error) { cmd, arg, err := singleConferencePrequel(ctxt) if cmd != "" { @@ -309,9 +321,72 @@ func NewTopic(ctxt ui.AmContext) (string, any, error) { return "redirect", urlStem, nil // no attachment - just redisplay topic list } - // TODO: bounce to the attachment form - _ = topic // TODO + post, err := topic.GetPost(0) // get the initial post in the new topic + if err != nil { + return ui.ErrorPage(ctxt, err) + } + + // go upload the attachment + ctxt.VarMap().Set("target", urlStem) + ctxt.VarMap().Set("post", post.PostId) + ctxt.VarMap().Set("amsterdam_pageTitle", "Upload Attachment") + return "framed_template", "attachment_upload.jet", nil } 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(postId) + if err == nil { + var data []byte + data, err = slurpFile(file) + if err == nil { + err = post.SetAttachment(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")) +} diff --git a/database/post.go b/database/post.go new file mode 100644 index 0000000..52e3ed0 --- /dev/null +++ b/database/post.go @@ -0,0 +1,51 @@ +/* + * Amsterdam Web Communities System + * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +// The database package contains database management and storage logic. +package database + +import ( + "errors" + "fmt" + "time" +) + +type PostHeader struct { + PostId int64 `db:"postid"` // ID of the post + Parent int64 `db:"parent"` // ID of parent (unused?) + TopicId int32 `db:"topicid"` // topic containing the post + Num int32 `db:"num"` // post number + LineCount *int32 `db:"linecount"` // number of lines + CreatorUid int32 `db:"creator_uid"` // UID creating post + Posted time.Time `db:"posted"` // date posted + Hidden bool `db:"hidden"` // is post hidden? + ScribbleUid *int32 `db:"scribble_uid"` // UID of who scribbled it + ScribbleDate *time.Time `db:"scribble_date"` // when was it scribbled? + Pseud *string `db:"pseud"` // post's "pseud" (name/header) +} + +func (p *PostHeader) SetAttachment(fileName string, mimeType string, length int32, data []byte) error { + _, err := amdb.Exec("INSERT INTO postattach (postid, datalen, filename, mimetype, data) VALUES (?, ?, ?, ?, ?)", + p.PostId, length, fileName, mimeType, data) + return err +} + +func AmGetPost(postId int64) (*PostHeader, error) { + var dbdata []PostHeader + err := amdb.Select(&dbdata, "SELECT * FROM posts WHERE postid = ?", postId) + if err != nil { + return nil, err + } + if len(dbdata) == 0 { + return nil, errors.New("post not found") + } + if len(dbdata) > 1 { + return nil, fmt.Errorf("AmGetPost: too many entries (%d) for post ID %d", len(dbdata), postId) + } + return &(dbdata[0]), nil +} diff --git a/database/topic.go b/database/topic.go index f284fc8..ce82d36 100644 --- a/database/topic.go +++ b/database/topic.go @@ -31,6 +31,25 @@ type Topic struct { Name string `db:"name"` // topic name } +// GetPost returns a post in the topic by number. +func (t *Topic) GetPost(num int32) (*PostHeader, error) { + if num > t.TopMessage { + return nil, fmt.Errorf("no post %d in topic %d", num, t.TopicId) + } + var dbdata []PostHeader + err := amdb.Select(&dbdata, "SELECT * FROM posts WHERE topicid = ? AND num = ?", t.TopicId, num) + if err == nil { + if len(dbdata) == 0 { + err = fmt.Errorf("no post %d in topic %d", num, t.TopicId) + } else if len(dbdata) > 1 { + err = fmt.Errorf("topic.GetPost: too many entries (%d) for post %d in topic %d", len(dbdata), num, t.TopicId) + } else { + return &(dbdata[0]), nil + } + } + return nil, err +} + // TopicSettings contains per-user settings for topics, including the "last read" message pointer. type TopicSettings struct { TopicId int32 `db:"topicid"` // unique ID of the topic @@ -44,18 +63,25 @@ type TopicSettings struct { // TopicSummary is a smaller data structure that gets topic information to create the topic list display. type TopicSummary struct { - TopicID int32 - Number int16 - Name string - Unread int32 - Total int32 - LastUpdate time.Time - Frozen bool - Archived bool - Subscribed bool - Hidden bool + TopicID int32 // the topic ID + Number int16 // the number of the topic + Name string // the topic name + Unread int32 // number of unread messages + Total int32 // total number of messages + LastUpdate time.Time // last update timestamp + Frozen bool // is topic frozen? + Archived bool // is topic archived? + Subscribed bool // is topic subscribed? + Hidden bool // is topic hidden? } +/* AmGetTopic retrieves a topic by ID. + * Parameters: + * topicId - ID of the topic to retrieve. + * Returns: + * The topic pointer, or nil. + * Standard Go error status. + */ func AmGetTopic(topicId int32) (*Topic, error) { var dbdata []Topic err := amdb.Select(&dbdata, "SELECT * FROM topics WHERE topicid = ?", topicId) diff --git a/main.go b/main.go index dc4eaec..b507b2f 100644 --- a/main.go +++ b/main.go @@ -74,6 +74,7 @@ func setupEcho() *echo.Echo { e.GET("/sysadmin", ui.AmWrap(SysAdminMenu)) e.GET("/create_comm", ui.AmWrap(CreateCommunityForm)) e.POST("/create_comm", ui.AmWrap(CreateCommunity)) + e.POST("/attachment_upload", ui.AmWrap(AttachmentUpload)) e.GET("/comm/:cid/profile", ui.AmWrap(ShowCommunity)) e.GET("/comm/:cid/join", ui.AmWrap(JoinCommunity)) e.POST("/comm/:cid/join", ui.AmWrap(JoinCommunityWithKey)) diff --git a/ui/views/attachment_upload.jet b/ui/views/attachment_upload.jet index 38a8c68..5f08eb7 100644 --- a/ui/views/attachment_upload.jet +++ b/ui/views/attachment_upload.jet @@ -28,7 +28,8 @@ {{ end }} -
+ +
@@ -50,7 +51,7 @@
@@ -62,7 +63,7 @@