Started working on the view upload page

This commit is contained in:
Leon Mika 2026-03-02 22:26:40 +11:00
parent 0a9af9cde8
commit 48f39133d7
18 changed files with 327 additions and 45 deletions

View file

@ -33,6 +33,7 @@ export default class UploadController extends Controller {
for (let file of files) { for (let file of files) {
await this._doUpload(file); await this._doUpload(file);
} }
window.location.reload();
} }
async _doUpload(file) { async _doUpload(file) {

View file

@ -124,6 +124,8 @@ Starting weiro without any arguments will start the server.
siteGroup.Delete("/posts/:postID", ph.Delete) siteGroup.Delete("/posts/:postID", ph.Delete)
siteGroup.Get("/uploads", uh.Index) siteGroup.Get("/uploads", uh.Index)
siteGroup.Get("/uploads/:uploadID", uh.Show)
siteGroup.Get("/uploads/:uploadID/raw", uh.ShowRaw)
siteGroup.Post("/uploads/pending", uh.New) siteGroup.Post("/uploads/pending", uh.New)
siteGroup.Post("/uploads/pending/:guid", uh.UploadPart) siteGroup.Post("/uploads/pending/:guid", uh.UploadPart)
siteGroup.Post("/uploads/pending/:guid/finalize", uh.UploadComplete) siteGroup.Post("/uploads/pending/:guid/finalize", uh.UploadComplete)

2
go.mod
View file

@ -23,6 +23,7 @@ require (
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
github.com/barasher/go-exiftool v1.10.0 // indirect
github.com/cenkalti/backoff/v4 v4.0.2 // indirect github.com/cenkalti/backoff/v4 v4.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
@ -80,6 +81,7 @@ require (
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.34.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
lmika.dev/pkg/modash v0.1.1-0.20260302110707-31c6b125c997 // indirect
modernc.org/libc v1.67.6 // indirect modernc.org/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect

6
go.sum
View file

@ -57,6 +57,8 @@ github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:o
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
github.com/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEXNMpIhs=
github.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
@ -731,6 +733,10 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lmika.dev/pkg/litemigrate v0.1.0 h1:DBEJahbQO7W3uEmAOQGg1URBWYimg0ClWHi83M2MZwk= lmika.dev/pkg/litemigrate v0.1.0 h1:DBEJahbQO7W3uEmAOQGg1URBWYimg0ClWHi83M2MZwk=
lmika.dev/pkg/litemigrate v0.1.0/go.mod h1:GQWWDiMZGQaVspcwKNq8vIBPN5H+KsUo/VBIeh9OfLg= lmika.dev/pkg/litemigrate v0.1.0/go.mod h1:GQWWDiMZGQaVspcwKNq8vIBPN5H+KsUo/VBIeh9OfLg=
lmika.dev/pkg/modash v0.1.0 h1:fltroSvP0nKj9K0E6G+S9LULvB9Qhj47+SZ2b9v/v/c=
lmika.dev/pkg/modash v0.1.0/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
lmika.dev/pkg/modash v0.1.1-0.20260302110707-31c6b125c997 h1:XGdi5Ca5IJgGXPd057R2QHENQ6PwIUUfhBTGGF6yuLM=
lmika.dev/pkg/modash v0.1.1-0.20260302110707-31c6b125c997/go.mod h1:8NDl/yR1eCCEhip9FJlVuMNXIeaztQ0Ks/tizExFcTI=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=

View file

@ -1,8 +1,15 @@
package handlers package handlers
import ( import (
"bufio"
"io"
"log"
"net/http"
"strconv"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
"lmika.dev/lmika/weiro/services/uploads" "lmika.dev/lmika/weiro/services/uploads"
"lmika.dev/pkg/modash/moslice"
) )
type UploadsHandler struct { type UploadsHandler struct {
@ -10,7 +17,65 @@ type UploadsHandler struct {
} }
func (uh UploadsHandler) Index(c fiber.Ctx) error { func (uh UploadsHandler) Index(c fiber.Ctx) error {
return c.Render("uploads/index", nil) uploads, err := uh.UploadsService.ListUploads(c.Context())
if err != nil {
return err
}
rows := moslice.Batch(uploads, 5)
return c.Render("uploads/index", fiber.Map{"uploads": rows})
}
func (uh UploadsHandler) Show(c fiber.Ctx) error {
uploadIDStr := c.Params("uploadID")
if uploadIDStr == "" {
return fiber.ErrBadRequest
}
uploadID, err := strconv.ParseInt(uploadIDStr, 10, 64)
if err != nil {
return fiber.ErrBadRequest
}
upload, err := uh.UploadsService.FetchUpload(c.Context(), uploadID)
if err != nil {
return err
}
return c.Render("uploads/show", fiber.Map{"upload": upload})
}
func (uh UploadsHandler) ShowRaw(c fiber.Ctx) error {
uploadIDStr := c.Params("uploadID")
if uploadIDStr == "" {
return fiber.ErrBadRequest
}
uploadID, err := strconv.ParseInt(uploadIDStr, 10, 64)
if err != nil {
return fiber.ErrBadRequest
}
upload, rwFn, err := uh.UploadsService.OpenUpload(c.Context(), uploadID)
if err != nil {
log.Print(err)
return err
}
c.Set("Content-Type", upload.MIMEType)
c.Status(http.StatusOK)
return c.SendStreamWriter(func(w *bufio.Writer) {
rw, err := rwFn()
if err != nil {
return
}
defer rw.Close()
_, err = io.Copy(w, rw)
if err != nil {
return
}
})
} }
func (uh UploadsHandler) New(c fiber.Ctx) error { func (uh UploadsHandler) New(c fiber.Ctx) error {

View file

@ -10,6 +10,7 @@ type Upload struct {
MIMEType string `json:"mime_type"` MIMEType string `json:"mime_type"`
Filename string `json:"filename"` Filename string `json:"filename"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
Slug string `json:"slug"`
Alt string `json:"alt"` Alt string `json:"alt"`
} }

View file

@ -5,7 +5,7 @@
package sqlgen package sqlgen
type PendingUpload struct { type PendingUpload struct {
ID interface{} ID int64
SiteID int64 SiteID int64
Guid string Guid string
UserID int64 UserID int64
@ -50,12 +50,13 @@ type Site struct {
} }
type Upload struct { type Upload struct {
ID interface{} ID int64
SiteID int64 SiteID int64
Guid string Guid string
MimeType string MimeType string
Filename string Filename string
FileSize int64 FileSize int64
Slug string
Alt string Alt string
CreatedAt int64 CreatedAt int64
} }

View file

@ -41,7 +41,7 @@ type InsertPendingUploadParams struct {
UploadStartedAt int64 UploadStartedAt int64
} }
func (q *Queries) InsertPendingUpload(ctx context.Context, arg InsertPendingUploadParams) (interface{}, error) { func (q *Queries) InsertPendingUpload(ctx context.Context, arg InsertPendingUploadParams) (int64, error) {
row := q.db.QueryRowContext(ctx, insertPendingUpload, row := q.db.QueryRowContext(ctx, insertPendingUpload,
arg.SiteID, arg.SiteID,
arg.Guid, arg.Guid,
@ -51,7 +51,7 @@ func (q *Queries) InsertPendingUpload(ctx context.Context, arg InsertPendingUplo
arg.MimeType, arg.MimeType,
arg.UploadStartedAt, arg.UploadStartedAt,
) )
var id interface{} var id int64
err := row.Scan(&id) err := row.Scan(&id)
return id, err return id, err
} }

View file

@ -13,7 +13,7 @@ const deleteUpload = `-- name: DeleteUpload :exec
DELETE FROM uploads WHERE id = ? DELETE FROM uploads WHERE id = ?
` `
func (q *Queries) DeleteUpload(ctx context.Context, id interface{}) error { func (q *Queries) DeleteUpload(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteUpload, id) _, err := q.db.ExecContext(ctx, deleteUpload, id)
return err return err
} }
@ -24,9 +24,11 @@ INSERT INTO uploads (
guid, guid,
mime_type, mime_type,
filename, filename,
created_at, file_size,
alt slug,
) VALUES (?, ?, ?, ?, ?, ?) alt,
created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id RETURNING id
` `
@ -35,8 +37,10 @@ type InsertUploadParams struct {
Guid string Guid string
MimeType string MimeType string
Filename string Filename string
CreatedAt int64 FileSize int64
Slug string
Alt string Alt string
CreatedAt int64
} }
func (q *Queries) InsertUpload(ctx context.Context, arg InsertUploadParams) error { func (q *Queries) InsertUpload(ctx context.Context, arg InsertUploadParams) error {
@ -45,17 +49,19 @@ func (q *Queries) InsertUpload(ctx context.Context, arg InsertUploadParams) erro
arg.Guid, arg.Guid,
arg.MimeType, arg.MimeType,
arg.Filename, arg.Filename,
arg.CreatedAt, arg.FileSize,
arg.Slug,
arg.Alt, arg.Alt,
arg.CreatedAt,
) )
return err return err
} }
const selectUploadByID = `-- name: SelectUploadByID :one const selectUploadByID = `-- name: SelectUploadByID :one
SELECT id, site_id, guid, mime_type, filename, file_size, alt, created_at FROM uploads WHERE id = ? SELECT id, site_id, guid, mime_type, filename, file_size, slug, alt, created_at FROM uploads WHERE id = ? LIMIT 1
` `
func (q *Queries) SelectUploadByID(ctx context.Context, id interface{}) (Upload, error) { func (q *Queries) SelectUploadByID(ctx context.Context, id int64) (Upload, error) {
row := q.db.QueryRowContext(ctx, selectUploadByID, id) row := q.db.QueryRowContext(ctx, selectUploadByID, id)
var i Upload var i Upload
err := row.Scan( err := row.Scan(
@ -65,6 +71,33 @@ func (q *Queries) SelectUploadByID(ctx context.Context, id interface{}) (Upload,
&i.MimeType, &i.MimeType,
&i.Filename, &i.Filename,
&i.FileSize, &i.FileSize,
&i.Slug,
&i.Alt,
&i.CreatedAt,
)
return i, err
}
const selectUploadBySiteIDAndSlug = `-- name: SelectUploadBySiteIDAndSlug :one
SELECT id, site_id, guid, mime_type, filename, file_size, slug, alt, created_at FROM uploads WHERE site_id = ? AND slug = ? LIMIT 1
`
type SelectUploadBySiteIDAndSlugParams struct {
SiteID int64
Slug string
}
func (q *Queries) SelectUploadBySiteIDAndSlug(ctx context.Context, arg SelectUploadBySiteIDAndSlugParams) (Upload, error) {
row := q.db.QueryRowContext(ctx, selectUploadBySiteIDAndSlug, arg.SiteID, arg.Slug)
var i Upload
err := row.Scan(
&i.ID,
&i.SiteID,
&i.Guid,
&i.MimeType,
&i.Filename,
&i.FileSize,
&i.Slug,
&i.Alt, &i.Alt,
&i.CreatedAt, &i.CreatedAt,
) )
@ -72,7 +105,7 @@ func (q *Queries) SelectUploadByID(ctx context.Context, id interface{}) (Upload,
} }
const selectUploadsOfSite = `-- name: SelectUploadsOfSite :many const selectUploadsOfSite = `-- name: SelectUploadsOfSite :many
SELECT id, site_id, guid, mime_type, filename, file_size, alt, created_at FROM uploads WHERE site_id = ? ORDER BY created_at DESC SELECT id, site_id, guid, mime_type, filename, file_size, slug, alt, created_at FROM uploads WHERE site_id = ? ORDER BY created_at DESC
` `
func (q *Queries) SelectUploadsOfSite(ctx context.Context, siteID int64) ([]Upload, error) { func (q *Queries) SelectUploadsOfSite(ctx context.Context, siteID int64) ([]Upload, error) {
@ -91,6 +124,7 @@ func (q *Queries) SelectUploadsOfSite(ctx context.Context, siteID int64) ([]Uplo
&i.MimeType, &i.MimeType,
&i.Filename, &i.Filename,
&i.FileSize, &i.FileSize,
&i.Slug,
&i.Alt, &i.Alt,
&i.CreatedAt, &i.CreatedAt,
); err != nil { ); err != nil {
@ -113,7 +147,7 @@ UPDATE uploads SET alt = ? WHERE id = ?
type UpdateUploadParams struct { type UpdateUploadParams struct {
Alt string Alt string
ID interface{} ID int64
} }
func (q *Queries) UpdateUpload(ctx context.Context, arg UpdateUploadParams) error { func (q *Queries) UpdateUpload(ctx context.Context, arg UpdateUploadParams) error {

View file

@ -30,6 +30,18 @@ func (db *Provider) SelectUploadsOfSite(ctx context.Context, siteID int64) ([]mo
return uploads, nil return uploads, nil
} }
func (db *Provider) SelectUploadBySiteIDAndSlug(ctx context.Context, siteID int64, slug string) (models.Upload, error) {
row, err := db.queries.SelectUploadBySiteIDAndSlug(ctx, sqlgen.SelectUploadBySiteIDAndSlugParams{
SiteID: siteID,
Slug: slug,
})
if err != nil {
return models.Upload{}, err
}
return dbUploadToUpload(row), nil
}
func (db *Provider) SaveUpload(ctx context.Context, upload *models.Upload) error { func (db *Provider) SaveUpload(ctx context.Context, upload *models.Upload) error {
if upload.ID == 0 { if upload.ID == 0 {
if err := db.queries.InsertUpload(ctx, sqlgen.InsertUploadParams{ if err := db.queries.InsertUpload(ctx, sqlgen.InsertUploadParams{
@ -37,8 +49,10 @@ func (db *Provider) SaveUpload(ctx context.Context, upload *models.Upload) error
Guid: upload.GUID, Guid: upload.GUID,
MimeType: upload.MIMEType, MimeType: upload.MIMEType,
Filename: upload.Filename, Filename: upload.Filename,
CreatedAt: upload.CreatedAt.Unix(), FileSize: upload.FileSize,
Slug: upload.Slug,
Alt: upload.Alt, Alt: upload.Alt,
CreatedAt: upload.CreatedAt.Unix(),
}); err != nil { }); err != nil {
return err return err
} }
@ -82,17 +96,14 @@ func (db *Provider) DeletePendingUpload(ctx context.Context, guid string) error
} }
func dbUploadToUpload(row sqlgen.Upload) models.Upload { func dbUploadToUpload(row sqlgen.Upload) models.Upload {
var id int64
if idVal, ok := row.ID.(int64); ok {
id = idVal
}
return models.Upload{ return models.Upload{
ID: id, ID: row.ID,
SiteID: row.SiteID, SiteID: row.SiteID,
GUID: row.Guid, GUID: row.Guid,
MIMEType: row.MimeType, MIMEType: row.MimeType,
FileSize: row.FileSize,
Filename: row.Filename, Filename: row.Filename,
Slug: row.Slug,
Alt: row.Alt, Alt: row.Alt,
CreatedAt: time.Unix(row.CreatedAt, 0).UTC(), CreatedAt: time.Unix(row.CreatedAt, 0).UTC(),
} }

View file

@ -0,0 +1,32 @@
package uploadfiles
import (
"emperror.dev/errors"
"github.com/barasher/go-exiftool"
"lmika.dev/lmika/weiro/models"
)
func (p *Provider) StripeEXIFData(site models.Site, up models.Upload) error {
uploadFilename := p.uploadFileName(site, up)
et, err := exiftool.NewExiftool()
if err != nil {
return err
}
defer et.Close()
fileInfos := et.ExtractMetadata(uploadFilename)
if len(fileInfos) == 0 {
return errors.New("no exif data found")
}
fileInfo := fileInfos[0]
fileInfo.ClearAll()
fileOut := []exiftool.FileMetadata{fileInfo}
et.WriteMetadata(fileOut)
if fileOut[0].Err != nil {
return fileOut[0].Err
}
return nil
}

View file

@ -1,7 +1,7 @@
package uploadfiles package uploadfiles
import ( import (
"fmt" "io"
"os" "os"
"path/filepath" "path/filepath"
@ -19,17 +19,24 @@ func New(baseDir string) *Provider {
} }
func (p *Provider) AdoptFile(site models.Site, up models.Upload, filename string) error { func (p *Provider) AdoptFile(site models.Site, up models.Upload, filename string) error {
baseDir := filepath.Join(p.baseDir, site.GUID, fullPath := p.uploadFileName(site, up)
fmt.Sprintf("%04d", up.CreatedAt.Year()), baseDir := filepath.Dir(fullPath)
fmt.Sprintf("%02d", up.CreatedAt.Month()))
if err := os.MkdirAll(baseDir, 0755); err != nil { if err := os.MkdirAll(baseDir, 0755); err != nil {
return err return err
} }
targetFilename := filepath.Join(baseDir, up.GUID) if err := os.Rename(filename, fullPath); err != nil {
if err := os.Rename(filename, targetFilename); err != nil {
return err return err
} }
return nil return nil
} }
func (p *Provider) OpenUpload(site models.Site, up models.Upload) (io.ReadCloser, error) {
fullPath := p.uploadFileName(site, up)
return os.Open(fullPath)
}
func (p *Provider) uploadFileName(site models.Site, up models.Upload) string {
return filepath.Join(p.baseDir, site.GUID, up.Slug)
}

View file

@ -0,0 +1,75 @@
package uploads
import (
"context"
"fmt"
"io"
"lmika.dev/lmika/weiro/models"
)
type UploadWithURL struct {
Upload models.Upload
URL string
}
func (s *Service) FetchUpload(ctx context.Context, uploadID int64) (res UploadWithURL, _ error) {
site, _, err := s.fetchSiteAndUser(ctx)
if err != nil {
return UploadWithURL{}, err
}
upload, err := s.db.SelectUploadByID(ctx, uploadID)
if err != nil {
return UploadWithURL{}, err
}
return UploadWithURL{
Upload: upload,
URL: fmt.Sprintf("/sites/%v/uploads/%v/raw", site.ID, upload.ID),
}, nil
}
func (s *Service) ListUploads(ctx context.Context) (res []UploadWithURL, _ error) {
site, _, err := s.fetchSiteAndUser(ctx)
if err != nil {
return nil, err
}
uploads, err := s.db.SelectUploadsOfSite(ctx, site.ID)
if err != nil {
return nil, err
}
res = make([]UploadWithURL, len(uploads))
for i, upload := range uploads {
res[i] = UploadWithURL{
Upload: upload,
URL: fmt.Sprintf("/sites/%v/uploads/%v/raw", site.ID, upload.ID),
}
}
return res, nil
}
func (s *Service) OpenUpload(ctx context.Context, id int64) (models.Upload, func() (io.ReadCloser, error), error) {
site, _, err := s.fetchSiteAndUser(ctx)
if err != nil {
return models.Upload{}, nil, err
}
upload, err := s.db.SelectUploadByID(ctx, id)
if err != nil {
return models.Upload{}, nil, err
} else if upload.SiteID != site.ID {
return models.Upload{}, nil, models.NotFoundError
}
return upload, func() (io.ReadCloser, error) {
rw, err := s.up.OpenUpload(site, upload)
if err != nil {
return nil, err
}
return rw, nil
}, nil
}

View file

@ -5,7 +5,9 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt"
"io" "io"
"log"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@ -100,13 +102,22 @@ func (s *Service) FinalizePending(ctx context.Context, pendingGUID string, expec
return err return err
} }
newUploadGUID := models.NewNanoID()
newTime := time.Now().UTC()
newSlug := filepath.Join(
fmt.Sprintf("%04d", newTime.Year()),
fmt.Sprintf("%02d", newTime.Month()),
newUploadGUID+filepath.Ext(pu.Filename),
)
newUpload := models.Upload{ newUpload := models.Upload{
SiteID: site.ID, SiteID: site.ID,
GUID: models.NewNanoID(), GUID: models.NewNanoID(),
FileSize: pu.FileSize, FileSize: pu.FileSize,
MIMEType: pu.MIMEType, MIMEType: pu.MIMEType,
Filename: pu.Filename, Filename: pu.Filename,
CreatedAt: time.Now().UTC(), CreatedAt: newTime,
Slug: newSlug,
} }
if err := s.db.SaveUpload(ctx, &newUpload); err != nil { if err := s.db.SaveUpload(ctx, &newUpload); err != nil {
return err return err
@ -115,6 +126,12 @@ func (s *Service) FinalizePending(ctx context.Context, pendingGUID string, expec
if err := s.up.AdoptFile(site, newUpload, pendingDataFilename); err != nil { if err := s.up.AdoptFile(site, newUpload, pendingDataFilename); err != nil {
return err return err
} }
if err := s.db.DeletePendingUpload(ctx, newUpload.GUID); err != nil {
return err
}
if err := s.up.StripeEXIFData(site, newUpload); err != nil {
log.Printf("warn: failed to extract exif data from %s: %v\n", newUpload.Slug, err)
}
return nil return nil
} }

View file

@ -2,7 +2,10 @@
SELECT * FROM uploads WHERE site_id = ? ORDER BY created_at DESC; SELECT * FROM uploads WHERE site_id = ? ORDER BY created_at DESC;
-- name: SelectUploadByID :one -- name: SelectUploadByID :one
SELECT * FROM uploads WHERE id = ?; SELECT * FROM uploads WHERE id = ? LIMIT 1;
-- name: SelectUploadBySiteIDAndSlug :one
SELECT * FROM uploads WHERE site_id = ? AND slug = ? LIMIT 1;
-- name: InsertUpload :exec -- name: InsertUpload :exec
INSERT INTO uploads ( INSERT INTO uploads (
@ -10,9 +13,11 @@ INSERT INTO uploads (
guid, guid,
mime_type, mime_type,
filename, filename,
created_at, file_size,
alt slug,
) VALUES (?, ?, ?, ?, ?, ?) alt,
created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id; RETURNING id;
-- name: UpdateUpload :exec -- name: UpdateUpload :exec

View file

@ -1,27 +1,28 @@
CREATE TABLE uploads ( CREATE TABLE uploads (
id SERIAL PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
site_id INT NOT NULL, site_id INT NOT NULL,
guid TEXT NOT NULL, guid TEXT NOT NULL,
mime_type TEXT NOT NULL, mime_type TEXT NOT NULL,
filename TEXT NOT NULL, filename TEXT NOT NULL,
file_size INT NOT NULL, file_size INT NOT NULL,
slug TEXT NOT NULL,
alt TEXT NOT NULL, alt TEXT NOT NULL,
created_at INT NOT NULL, created_at INT NOT NULL,
FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE
); );
CREATE INDEX idx_uploads_site ON uploads (site_id); CREATE INDEX idx_uploads_site ON uploads (site_id);
CREATE UNIQUE INDEX idx_uploads_guid ON sites (guid); CREATE UNIQUE INDEX idx_uploads_guid ON uploads (guid);
CREATE UNIQUE INDEX idx_uploads_site_slug ON uploads (site_id, slug);
CREATE TABLE pending_uploads ( CREATE TABLE pending_uploads (
id SERIAL PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
site_id INT NOT NULL, site_id INT NOT NULL,
guid TEXT NOT NULL, guid TEXT NOT NULL,
user_id INT NOT NULL, user_id INT NOT NULL,
filename TEXT NOT NULL, filename TEXT NOT NULL,
file_size INT NOT NULL, file_size INT NOT NULL,
mime_type TEXT NOT NULL, mime_type TEXT NOT NULL,
upload_started_at INT NOT NULL, upload_started_at INT NOT NULL,
FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE, FOREIGN KEY (site_id) REFERENCES sites (id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
); );

View file

@ -3,4 +3,18 @@
data-controller="upload" data-upload-site-id-value="{{ .site.ID }}"> data-controller="upload" data-upload-site-id-value="{{ .site.ID }}">
<button class="btn btn-success" data-action="upload#upload">Upload</button> <button class="btn btn-success" data-action="upload#upload">Upload</button>
</div> </div>
{{ range .uploads }}
<div class="row row-cols-5">
{{ range . }}
<div class="col">
<a href="/sites/{{ $.site.ID }}/uploads/{{ .Upload.ID }}">
<div class="ratio ratio-1x1 m-2">
<img src="{{ .URL }}" alt="{{ .Upload.Alt }}" class="img-fluid object-fit-contain">
</div>
</a>
</div>
{{ end }}
</div>
{{ end }}
</main> </main>

8
views/uploads/show.html Normal file
View file

@ -0,0 +1,8 @@
<main class="container">
<div class="my-4 d-flex justify-content-between align-items-baseline"
data-controller="upload" data-upload-site-id-value="{{ .site.ID }}">
<button class="btn" data-action="upload#upload">Copy HTML</button>
</div>
<img src="{{ .URL }}" alt="{{ .Upload.Alt }}" class="img-fluid">
</main>