put together the handling for photo uploaded page (untested)
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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 (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ImageStore is the structure for the image store table.
|
||||
type ImageStore struct {
|
||||
ImgId int32 `db:"imgid"`
|
||||
TypeCode int16 `db:"typecode"`
|
||||
OwnerID *int32 `db:"ownerid"`
|
||||
MimeType string `db:"mimetype"`
|
||||
Length int32 `db:"length"`
|
||||
Data []byte `db:"data"`
|
||||
}
|
||||
|
||||
// Values for the TypeCode field of ImageStore.
|
||||
const (
|
||||
ImageTypeUserPhoto = int16(1)
|
||||
ImageTypeCommunityLogo = int16(2)
|
||||
)
|
||||
|
||||
// Save persists the ImageStore record to the database.
|
||||
func (img *ImageStore) Save() error {
|
||||
var err error
|
||||
if img.ImgId > 0 {
|
||||
_, err = amdb.NamedExec(`UPDATE imagestore SET typecode = :typecode, ownerid = :ownerid, mimetype = :mimetype,
|
||||
length = :length, data = :data WHERE imgid = :imgid`, img)
|
||||
} else {
|
||||
var rs sql.Result
|
||||
rs, err = amdb.NamedExec(`INSERT INTO imagestore (typecode, ownerid, mimetype, length, data)
|
||||
VALUES (:typecode, :ownerid, :mimetype, :length, :data)`, img)
|
||||
if err == nil {
|
||||
var lii int64
|
||||
lii, err = rs.LastInsertId()
|
||||
if err == nil {
|
||||
img.ImgId = int32(lii)
|
||||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
/* AmLoadImage loads an image from the database.
|
||||
* Parameters:
|
||||
* id - The ID of the image to be loaded.
|
||||
* Returns:
|
||||
* Pointer to ImageStore, or nil.
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func AmLoadImage(id int32) (*ImageStore, error) {
|
||||
var dbdata []ImageStore
|
||||
err := amdb.Select(&dbdata, "SELECT * FROM imagestore WHERE imgid = ?", id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(dbdata) == 0 {
|
||||
return nil, fmt.Errorf("image ID %d not found", id)
|
||||
} else if len(dbdata) > 1 {
|
||||
return nil, fmt.Errorf("image ID %d too many images (%d)", id, len(dbdata))
|
||||
}
|
||||
return &(dbdata[0]), nil
|
||||
}
|
||||
|
||||
/* AmStoreImage stores an image in the database, overwriting one with the same type code and owner if it exists.
|
||||
* Parameters:
|
||||
* typecode - Type code for the image.
|
||||
* owner - Owner Id for the image (UID or community ID)
|
||||
* mimetype - MIME type of the image.
|
||||
* data - Bytes of the actual image.
|
||||
* Returns:
|
||||
* Pointer to ImageStore, or nil.
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func AmStoreImage(typecode int16, owner int32, mimetype string, data []byte) (*ImageStore, error) {
|
||||
rs, err := amdb.Query("SELECT imgid FROM imagestore WHERE typecode = ? AND ownerid = ?", typecode, owner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var img *ImageStore
|
||||
if rs.Next() {
|
||||
var id int32
|
||||
rs.Scan(&id)
|
||||
img, err = AmLoadImage(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
img.MimeType = mimetype
|
||||
img.Length = int32(len(data))
|
||||
img.Data = data
|
||||
} else {
|
||||
img = &ImageStore{
|
||||
ImgId: -1,
|
||||
TypeCode: typecode,
|
||||
OwnerID: &owner,
|
||||
MimeType: mimetype,
|
||||
Length: int32(len(data)),
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
err = img.Save()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
/* AmDeleteImage erases an image from the database.
|
||||
* Parameters:
|
||||
* id - The ID of the image to be deleted.
|
||||
* Returns:
|
||||
* Standard Go error status.
|
||||
*/
|
||||
func AmDeleteImage(id int32) error {
|
||||
_, err := amdb.Exec("DELETE FROM imagestore WHERE imgid = ?", id)
|
||||
return err
|
||||
}
|
||||
@@ -7,6 +7,7 @@ require (
|
||||
github.com/alexflint/go-arg v1.6.0
|
||||
github.com/biter777/countries v1.7.5
|
||||
github.com/bits-and-blooms/bitset v1.24.0
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/hashicorp/golang-lru v1.0.2
|
||||
@@ -31,6 +32,7 @@ require (
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
|
||||
@@ -15,6 +15,8 @@ github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
@@ -61,12 +63,15 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
|
||||
@@ -55,6 +55,7 @@ func setupEcho() *echo.Echo {
|
||||
e.GET("/profile", ui.AmWrap(EditProfileForm))
|
||||
e.POST("/profile", ui.AmWrap(EditProfile))
|
||||
e.GET("/profile_photo", ui.AmWrap(ProfilePhotoForm))
|
||||
e.POST("/profile_photo", ui.AmWrap(ProfilePhoto))
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ package ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -34,6 +35,7 @@ type AmContext interface {
|
||||
FormField(string) string
|
||||
FormFieldInt(string) (int, error)
|
||||
FormFieldIsSet(string) bool
|
||||
FormFile(string) (*multipart.FileHeader, error)
|
||||
RC() int
|
||||
OutputType() string
|
||||
Parameter(string) string
|
||||
@@ -127,6 +129,11 @@ func (c *amContext) FormFieldIsSet(name string) bool {
|
||||
return req.Form.Has(name)
|
||||
}
|
||||
|
||||
// FormFile returns a "file" parameter from a multipart upload form.
|
||||
func (c *amContext) FormFile(name string) (*multipart.FileHeader, error) {
|
||||
return c.echoContext.FormFile(name)
|
||||
}
|
||||
|
||||
// RC returns the HTTP result code for the current operation.
|
||||
func (c *amContext) RC() int {
|
||||
return c.httprc
|
||||
|
||||
+64
-2
@@ -11,12 +11,22 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
"github.com/disintegration/imaging"
|
||||
)
|
||||
|
||||
//go:embed static_images/*
|
||||
@@ -43,17 +53,69 @@ func mimeTypeFromFilename(filename string) string {
|
||||
func AmServeImage(ctxt AmContext) (string, any, error) {
|
||||
components := strings.SplitAfter(ctxt.URLPath(), "/")
|
||||
var err error = nil
|
||||
var b []byte
|
||||
if len(components) == 4 {
|
||||
if components[2] == "builtin/" {
|
||||
switch components[2] {
|
||||
case "builtin/":
|
||||
var b []byte
|
||||
b, err = static_images.ReadFile(filepath.Join("static_images", components[3]))
|
||||
if err == nil {
|
||||
ctxt.SetOutputType(mimeTypeFromFilename(components[3]))
|
||||
return "bytes", b, nil
|
||||
}
|
||||
case "store/":
|
||||
var id int
|
||||
id, err = strconv.Atoi(components[3])
|
||||
if err == nil {
|
||||
var img *database.ImageStore
|
||||
img, err = database.AmLoadImage(int32(id))
|
||||
if err == nil {
|
||||
ctxt.SetOutputType(img.MimeType)
|
||||
return "bytes", img.Data, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ctxt.SetRC(http.StatusNotFound)
|
||||
// TODO: improve this error reporting
|
||||
return "string", fmt.Sprintf("File not found: %s", ctxt.URLPath()), err
|
||||
}
|
||||
|
||||
func AmProcessUploadedImage(fileheader *multipart.FileHeader, width, height int) ([]byte, string, error) {
|
||||
// open the file
|
||||
file, err := fileheader.Open()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// load the image from the file
|
||||
img, format, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// resize the image using high-quality Lanczos filter
|
||||
resized := imaging.Resize(img, width, height, imaging.Lanczos)
|
||||
|
||||
// re-encode it to the original format, or JPEG if that's not possible
|
||||
var buf bytes.Buffer
|
||||
var outType string
|
||||
switch strings.ToLower(format) {
|
||||
case "jpeg", "jpg":
|
||||
err = jpeg.Encode(&buf, resized, &jpeg.Options{Quality: 90})
|
||||
outType = "image/jpeg"
|
||||
case "gif":
|
||||
err = gif.Encode(&buf, resized, &gif.Options{NumColors: 256})
|
||||
outType = "image/gif"
|
||||
case "png":
|
||||
err = png.Encode(&buf, resized)
|
||||
outType = "image/png"
|
||||
default:
|
||||
err = jpeg.Encode(&buf, resized, &jpeg.Options{Quality: 90})
|
||||
outType = "image/jpeg"
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return buf.Bytes(), outType, nil
|
||||
}
|
||||
|
||||
@@ -13,6 +13,20 @@
|
||||
<hr class="border-2 border-gray-400 w-4/5 mb-4">
|
||||
</div>
|
||||
|
||||
{{ if isset(errorMessage) }}
|
||||
<!-- Error Message Banner -->
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6" id="error-banner">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="text-red-500 text-xl">⚠️</span>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium" id="error-message">{{ CapitalizeString(errorMessage) }}.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- Upload Form -->
|
||||
<form method="POST" enctype="multipart/form-data" action="/profile_photo" class="max-w-2xl">
|
||||
<input type="hidden" name="tgt" value="{{ target }}">
|
||||
|
||||
+73
@@ -11,11 +11,15 @@ package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.erbosoft.com/amy/amsterdam/database"
|
||||
"git.erbosoft.com/amy/amsterdam/ui"
|
||||
"git.erbosoft.com/amy/amsterdam/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
/* EditProfileForm renders the Amsterdam profile editing form.
|
||||
@@ -205,3 +209,72 @@ func ProfilePhotoForm(ctxt ui.AmContext) (string, any, error) {
|
||||
}
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
|
||||
func ProfilePhoto(ctxt ui.AmContext) (string, any, error) {
|
||||
u := ctxt.CurrentUser()
|
||||
if u.IsAnon {
|
||||
return ui.ErrorPage(ctxt, errors.New("you are not logged in"))
|
||||
}
|
||||
ci, err := u.ContactInfo()
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
target := ctxt.FormField("tgt")
|
||||
if target == "" {
|
||||
target = "/"
|
||||
}
|
||||
if ctxt.FormFieldIsSet("cancel") {
|
||||
return "redirect", "/profile?tgt=" + url.QueryEscape(target), nil
|
||||
}
|
||||
if ctxt.FormFieldIsSet("upload") {
|
||||
file, err := ctxt.FormFile("thepic")
|
||||
if err == nil {
|
||||
imageData, mimeType, err := ui.AmProcessUploadedImage(file, 200, 200)
|
||||
if err == nil {
|
||||
var img *database.ImageStore
|
||||
img, err = database.AmStoreImage(database.ImageTypeUserPhoto, u.Uid, mimeType, imageData)
|
||||
if err == nil {
|
||||
photourl := fmt.Sprintf("/img/store/%d", img.ImgId)
|
||||
ci.PhotoURL = &photourl
|
||||
_, err = ci.Save()
|
||||
if err == nil {
|
||||
return "redirect", "/profile?tgt=" + url.QueryEscape(target), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ctxt.VarMap().Set("errorMessage", err.Error())
|
||||
ctxt.VarMap().Set("target", target)
|
||||
ctxt.VarMap().Set("photo_url", "/img/builtin/no-user.png")
|
||||
ctxt.VarMap().Set("amsterdam_pageTitle", "Upload User Photo")
|
||||
return "framed_template", "photo_upload.jet", nil
|
||||
}
|
||||
if ctxt.FormFieldIsSet("remove") {
|
||||
purl := ci.PhotoURL
|
||||
if purl == nil || *purl == "" {
|
||||
// this is a no-op
|
||||
return "redirect", "/profile?tgt=" + url.QueryEscape(target), nil
|
||||
}
|
||||
if strings.HasPrefix(*purl, "/img/store/") {
|
||||
id, err := strconv.Atoi((*purl)[11:])
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
defer func() {
|
||||
go func() {
|
||||
err := database.AmDeleteImage(int32(id))
|
||||
if err != nil {
|
||||
log.Errorf("unable to delete image ID %d: %v", id, err)
|
||||
}
|
||||
}()
|
||||
}()
|
||||
}
|
||||
ci.PhotoURL = nil
|
||||
_, err := ci.Save()
|
||||
if err != nil {
|
||||
return ui.ErrorPage(ctxt, err)
|
||||
}
|
||||
return "redirect", "/profile?tgt=" + url.QueryEscape(target), nil
|
||||
}
|
||||
return ui.ErrorPage(ctxt, errors.New("invalid button detected in photo upload"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user