More changes to uploads:

- Have got upload images appearing in the post list
- Allowed for deleting uploads
- Allowed for seeing the upload progress
- Fixed the setting of upload properties like the MIME type
- Removed the stripe exif logic with just re-encoding PNGs and JPEGs by loading them and saving them
This commit is contained in:
Leon Mika 2026-03-04 22:33:39 +11:00
parent d0cebe6564
commit 199ff9feb9
21 changed files with 471 additions and 65 deletions

View file

@ -0,0 +1,27 @@
package markdown
import (
"context"
"strings"
"github.com/PuerkitoBio/goquery"
)
type htmlTransform func(ctx context.Context, dom *goquery.Document) error
func applyTransforms(ctx context.Context, inHTML string, transforms []htmlTransform) string {
dom, err := goquery.NewDocumentFromReader(strings.NewReader(inHTML))
if err != nil {
return inHTML
}
for _, transform := range transforms {
if err := transform(ctx, dom); err != nil {
return inHTML
}
}
res, err := dom.Html()
if err != nil {
return inHTML
}
return res
}

View file

@ -0,0 +1,86 @@
package markdown
import (
"context"
"fmt"
"io"
"strings"
"github.com/microcosm-cc/bluemonday"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
gm_html "github.com/yuin/goldmark/renderer/html"
"lmika.dev/lmika/weiro/models"
)
type Renderer struct {
mdParser goldmark.Markdown
bmPolicy *bluemonday.Policy
htmlTransforms []htmlTransform
}
func NewRendererForUI() *Renderer {
mdParser := goldmark.New(
goldmark.WithExtensions(extension.GFM),
goldmark.WithRendererOptions(
gm_html.WithUnsafe(),
),
)
bmPolicy := bluemonday.UGCPolicy()
return &Renderer{
mdParser: mdParser,
bmPolicy: bmPolicy,
htmlTransforms: []htmlTransform{
rewriteImgSrc(func(ctx context.Context, src string) string {
if strings.HasPrefix(src, "/uploads/") {
if site, ok := models.GetSite(ctx); ok {
return fmt.Sprintf("/sites/%v/uploads/slug/%v", site.ID, strings.TrimPrefix(src, "/uploads/"))
}
return src
}
return src
}),
},
}
}
func NewRendererForSite() *Renderer {
mdParser := goldmark.New(
goldmark.WithExtensions(extension.GFM),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
gm_html.WithUnsafe(),
),
)
return &Renderer{
mdParser: mdParser,
}
}
func (r *Renderer) Render(ctx context.Context, in string) (string, error) {
var sb strings.Builder
if err := r.mdParser.Convert([]byte(in), &sb); err != nil {
return "", err
}
outHTML := applyTransforms(ctx, sb.String(), r.htmlTransforms)
if r.bmPolicy != nil {
return r.bmPolicy.Sanitize(outHTML), nil
}
return sb.String(), nil
}
func (r *Renderer) RenderTo(ctx context.Context, w io.Writer, in string) error {
s, err := r.Render(ctx, in)
if err != nil {
return err
}
_, err = w.Write([]byte(s))
return err
}

View file

@ -0,0 +1,21 @@
package markdown
import (
"context"
"github.com/PuerkitoBio/goquery"
)
func rewriteImgSrc(transform func(ctx context.Context, in string) string) htmlTransform {
return func(ctx context.Context, dom *goquery.Document) error {
dom.Find("img").Each(func(i int, s *goquery.Selection) {
s.SetAttr("src", transform(ctx, s.AttrOr("src", "")))
})
dom.Find("img").Each(func(i int, s *goquery.Selection) {
s.AddClass("img-fluid")
})
//SetAttr("style", "max-width: 200px;max-height: 200px;height: auto;")
//log.Printf("Rewritten image src: %s", s.AttrOr("style", ""))
return nil
}
}

View file

@ -2,6 +2,7 @@ package sitebuilder
import (
"bytes"
"context"
"fmt"
"html/template"
"io"
@ -10,18 +11,15 @@ import (
"sort"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"golang.org/x/sync/errgroup"
"lmika.dev/lmika/weiro/models"
"lmika.dev/lmika/weiro/models/pubmodel"
"lmika.dev/lmika/weiro/providers/markdown"
)
type Builder struct {
site pubmodel.Site
gmMarkdown goldmark.Markdown
mdRenderer *markdown.Renderer
opts Options
tmpls *template.Template
}
@ -35,18 +33,10 @@ func New(site pubmodel.Site, opts Options) (*Builder, error) {
}
return &Builder{
site: site,
opts: opts,
tmpls: tmpls,
gmMarkdown: goldmark.New(
goldmark.WithExtensions(extension.GFM),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
),
site: site,
opts: opts,
tmpls: tmpls,
mdRenderer: markdown.NewRendererForSite(),
}, nil
}
@ -121,7 +111,7 @@ func (b *Builder) renderPost(post *models.Post) (postSingleData, error) {
}
var md bytes.Buffer
if err := b.gmMarkdown.Convert([]byte(post.Body), &md); err != nil {
if err := b.mdRenderer.RenderTo(context.Background(), &md, post.Body); err != nil {
return postSingleData{}, fmt.Errorf("failed to write post %s: %w", post.Slug, err)
}

View file

@ -1,31 +1,57 @@
package uploadfiles
import (
"os"
"path/filepath"
"strings"
"emperror.dev/errors"
"github.com/barasher/go-exiftool"
"github.com/disintegration/imaging"
"lmika.dev/lmika/weiro/models"
)
const (
applicationOctetStream = "application/octet-stream"
)
var supportedRencodableImageTypes = map[string]bool{
"image/jpeg": true,
"image/png": true,
applicationOctetStream: true,
}
var supportedReencoableExtensions = map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
}
func (p *Provider) StripeEXIFData(site models.Site, up models.Upload) error {
uploadFilename := p.uploadFileName(site, up)
et, err := exiftool.NewExiftool()
if !supportedRencodableImageTypes[up.MIMEType] {
return errors.New("unsupported image format: " + up.MIMEType)
}
if up.MIMEType == applicationOctetStream && !supportedReencoableExtensions[filepath.Ext(uploadFilename)] {
return errors.New("unsupported image format")
}
img, err := imaging.Open(uploadFilename)
if err != nil {
return err
return errors.Wrap(err, "failed to open image file")
}
defer et.Close()
fileInfos := et.ExtractMetadata(uploadFilename)
if len(fileInfos) == 0 {
return errors.New("no exif data found")
tmpName := strings.TrimSuffix(uploadFilename, filepath.Ext(uploadFilename)) + ".tmp." + filepath.Ext(uploadFilename)
if err := imaging.Save(img, tmpName); err != nil {
return errors.Wrap(err, "failed to save image file")
}
fileInfo := fileInfos[0]
fileInfo.ClearAll()
fileOut := []exiftool.FileMetadata{fileInfo}
et.WriteMetadata(fileOut)
if fileOut[0].Err != nil {
return fileOut[0].Err
if err := os.Remove(uploadFilename); err != nil {
_ = os.Remove(tmpName)
return errors.Wrap(err, "failed to remove image file")
}
if err := os.Rename(tmpName, uploadFilename); err != nil {
return errors.Wrap(err, "failed to rename image file")
}
return nil

View file

@ -37,6 +37,17 @@ func (p *Provider) OpenUpload(site models.Site, up models.Upload) (io.ReadCloser
return os.Open(fullPath)
}
func (p *Provider) DeleteUpload(site models.Site, up models.Upload) error {
fullPath := p.uploadFileName(site, up)
if err := os.Remove(fullPath); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
return nil
}
func (p *Provider) UploadDir(site models.Site) string {
return filepath.Join(p.baseDir, site.GUID)
}