wrote the code to handle attachment upload (and link from new-topic post)
This commit is contained in:
+77
-2
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+36
-10
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
{{ end }}
|
||||
|
||||
<!-- Upload Form -->
|
||||
<form method="POST" enctype="multipart/form-data" action="/TODO" class="max-w-2xl">
|
||||
<form method="POST" enctype="multipart/form-data" action="/attachment_upload" class="max-w-2xl">
|
||||
<input type="hidden" name="post" value="{{ post }}">
|
||||
<input type="hidden" name="tgt" value="{{ target }}">
|
||||
|
||||
<div class="bg-gray-50 p-6 rounded-lg">
|
||||
@@ -50,7 +51,7 @@
|
||||
<div class="flex gap-4">
|
||||
<button type="submit" name="upload"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">
|
||||
Upload Photo
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,7 +63,7 @@
|
||||
<ul class="text-xs text-blue-800 space-y-1 list-disc list-inside">
|
||||
<li>The attachment will remain as part of the post.</li>
|
||||
<li>Be sure you have the right to upload your attachment publicly.</li>
|
||||
<li>Inapproriate attachments may cause your post to be removed from the conference.</li>
|
||||
<li>Inappropriate attachments may cause your post to be removed from the conference.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user