Uploads #1

Merged
lmika merged 6 commits from feature/uploads into main 2026-03-05 10:03:47 +00:00
11 changed files with 101 additions and 15 deletions
Showing only changes of commit 0a9af9cde8 - Show all commits

View file

@ -3,13 +3,14 @@ package models
import "time" import "time"
type Upload struct { type Upload struct {
ID int64 `json:"id"` ID int64 `json:"id"`
SiteID int64 `json:"site_id"` SiteID int64 `json:"site_id"`
GUID string `json:"guid"` GUID string `json:"guid"`
MIMEType string `json:"mime_type"` FileSize int64 `json:"file_size"`
Filename string `json:"filename"` MIMEType string `json:"mime_type"`
CreatedAt int64 `json:"created_at"` Filename string `json:"filename"`
Alt string `json:"alt"` CreatedAt time.Time `json:"created_at"`
Alt string `json:"alt"`
} }
type PendingUpload struct { type PendingUpload struct {

View file

@ -55,6 +55,7 @@ type Upload struct {
Guid string Guid string
MimeType string MimeType string
Filename string Filename string
FileSize int64
Alt string Alt string
CreatedAt int64 CreatedAt int64
} }

View file

@ -9,6 +9,15 @@ import (
"context" "context"
) )
const deletePendingUpload = `-- name: DeletePendingUpload :exec
DELETE FROM pending_uploads WHERE guid = ?
`
func (q *Queries) DeletePendingUpload(ctx context.Context, guid string) error {
_, err := q.db.ExecContext(ctx, deletePendingUpload, guid)
return err
}
const insertPendingUpload = `-- name: InsertPendingUpload :one const insertPendingUpload = `-- name: InsertPendingUpload :one
INSERT INTO pending_uploads ( INSERT INTO pending_uploads (
site_id, site_id,

View file

@ -52,7 +52,7 @@ func (q *Queries) InsertUpload(ctx context.Context, arg InsertUploadParams) erro
} }
const selectUploadByID = `-- name: SelectUploadByID :one const selectUploadByID = `-- name: SelectUploadByID :one
SELECT id, site_id, guid, mime_type, filename, alt, created_at FROM uploads WHERE id = ? SELECT id, site_id, guid, mime_type, filename, file_size, alt, created_at FROM uploads WHERE id = ?
` `
func (q *Queries) SelectUploadByID(ctx context.Context, id interface{}) (Upload, error) { func (q *Queries) SelectUploadByID(ctx context.Context, id interface{}) (Upload, error) {
@ -64,6 +64,7 @@ func (q *Queries) SelectUploadByID(ctx context.Context, id interface{}) (Upload,
&i.Guid, &i.Guid,
&i.MimeType, &i.MimeType,
&i.Filename, &i.Filename,
&i.FileSize,
&i.Alt, &i.Alt,
&i.CreatedAt, &i.CreatedAt,
) )
@ -71,7 +72,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, alt, created_at FROM uploads WHERE site_id = ? ORDER BY created_at DESC SELECT id, site_id, guid, mime_type, filename, file_size, 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) {
@ -89,6 +90,7 @@ func (q *Queries) SelectUploadsOfSite(ctx context.Context, siteID int64) ([]Uplo
&i.Guid, &i.Guid,
&i.MimeType, &i.MimeType,
&i.Filename, &i.Filename,
&i.FileSize,
&i.Alt, &i.Alt,
&i.CreatedAt, &i.CreatedAt,
); err != nil { ); err != nil {

View file

@ -37,7 +37,7 @@ 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, CreatedAt: upload.CreatedAt.Unix(),
Alt: upload.Alt, Alt: upload.Alt,
}); err != nil { }); err != nil {
return err return err
@ -77,6 +77,10 @@ func (db *Provider) SavePendingUpload(ctx context.Context, pending *models.Pendi
return err return err
} }
func (db *Provider) DeletePendingUpload(ctx context.Context, guid string) error {
return db.queries.DeletePendingUpload(ctx, guid)
}
func dbUploadToUpload(row sqlgen.Upload) models.Upload { func dbUploadToUpload(row sqlgen.Upload) models.Upload {
var id int64 var id int64
if idVal, ok := row.ID.(int64); ok { if idVal, ok := row.ID.(int64); ok {
@ -90,7 +94,7 @@ func dbUploadToUpload(row sqlgen.Upload) models.Upload {
MIMEType: row.MimeType, MIMEType: row.MimeType,
Filename: row.Filename, Filename: row.Filename,
Alt: row.Alt, Alt: row.Alt,
CreatedAt: row.CreatedAt, CreatedAt: time.Unix(row.CreatedAt, 0).UTC(),
} }
} }

View file

@ -0,0 +1,35 @@
package uploadfiles
import (
"fmt"
"os"
"path/filepath"
"lmika.dev/lmika/weiro/models"
)
type Provider struct {
baseDir string
}
func New(baseDir string) *Provider {
return &Provider{
baseDir: baseDir,
}
}
func (p *Provider) AdoptFile(site models.Site, up models.Upload, filename string) error {
baseDir := filepath.Join(p.baseDir, site.GUID,
fmt.Sprintf("%04d", up.CreatedAt.Year()),
fmt.Sprintf("%02d", up.CreatedAt.Month()))
if err := os.MkdirAll(baseDir, 0755); err != nil {
return err
}
targetFilename := filepath.Join(baseDir, up.GUID)
if err := os.Rename(filename, targetFilename); err != nil {
return err
}
return nil
}

View file

@ -5,6 +5,7 @@ import (
"lmika.dev/lmika/weiro/config" "lmika.dev/lmika/weiro/config"
"lmika.dev/lmika/weiro/providers/db" "lmika.dev/lmika/weiro/providers/db"
"lmika.dev/lmika/weiro/providers/uploadfiles"
"lmika.dev/lmika/weiro/services/auth" "lmika.dev/lmika/weiro/services/auth"
"lmika.dev/lmika/weiro/services/posts" "lmika.dev/lmika/weiro/services/posts"
"lmika.dev/lmika/weiro/services/publisher" "lmika.dev/lmika/weiro/services/publisher"
@ -28,12 +29,14 @@ func New(cfg config.Config) (*Services, error) {
return nil, err return nil, err
} }
ufp := uploadfiles.New(filepath.Join(cfg.DataDir, "uploads"))
authSvc := auth.New(dbp) authSvc := auth.New(dbp)
publisherSvc := publisher.New(dbp) publisherSvc := publisher.New(dbp)
publisherQueue := publisher.NewQueue(publisherSvc) publisherQueue := publisher.NewQueue(publisherSvc)
postService := posts.New(dbp, publisherQueue) postService := posts.New(dbp, publisherQueue)
siteService := sites.New(dbp) siteService := sites.New(dbp)
uploadService := uploads.New(dbp, filepath.Join(cfg.ScratchDir, "uploads", "pending")) uploadService := uploads.New(dbp, ufp, filepath.Join(cfg.ScratchDir, "uploads", "pending"))
return &Services{ return &Services{
DB: dbp, DB: dbp,

View file

@ -95,12 +95,36 @@ func (s *Service) FinalizePending(ctx context.Context, pendingGUID string, expec
return errors.New("invalid pending upload") return errors.New("invalid pending upload")
} }
pendingDataFilename := filepath.Join(s.pendingDir, pu.GUID+".upload")
if err := s.verifyPendingUpload(pendingDataFilename, expectedHash); err != nil {
return err
}
newUpload := models.Upload{
SiteID: site.ID,
GUID: models.NewNanoID(),
FileSize: pu.FileSize,
MIMEType: pu.MIMEType,
Filename: pu.Filename,
CreatedAt: time.Now().UTC(),
}
if err := s.db.SaveUpload(ctx, &newUpload); err != nil {
return err
}
if err := s.up.AdoptFile(site, newUpload, pendingDataFilename); err != nil {
return err
}
return nil
}
func (s *Service) verifyPendingUpload(pendingDataFilename string, expectedHash string) error {
expectedHashBytes, err := hex.DecodeString(expectedHash) expectedHashBytes, err := hex.DecodeString(expectedHash)
if err != nil { if err != nil {
return err return err
} }
pendingDataFilename := filepath.Join(s.pendingDir, pu.GUID+".upload")
if _, err := os.Stat(pendingDataFilename); err != nil { if _, err := os.Stat(pendingDataFilename); err != nil {
return err return err
} }

View file

@ -5,16 +5,19 @@ import (
"lmika.dev/lmika/weiro/models" "lmika.dev/lmika/weiro/models"
"lmika.dev/lmika/weiro/providers/db" "lmika.dev/lmika/weiro/providers/db"
"lmika.dev/lmika/weiro/providers/uploadfiles"
) )
type Service struct { type Service struct {
db *db.Provider db *db.Provider
up *uploadfiles.Provider
pendingDir string pendingDir string
} }
func New(db *db.Provider, pendingDir string) *Service { func New(db *db.Provider, up *uploadfiles.Provider, pendingDir string) *Service {
return &Service{ return &Service{
db: db, db: db,
up: up,
pendingDir: pendingDir, pendingDir: pendingDir,
} }
} }

View file

@ -11,4 +11,7 @@ INSERT INTO pending_uploads (
mime_type, mime_type,
upload_started_at upload_started_at
) VALUES (?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?)
RETURNING id; RETURNING id;
-- name: DeletePendingUpload :exec
DELETE FROM pending_uploads WHERE guid = ?;

View file

@ -4,6 +4,7 @@ CREATE TABLE uploads (
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,
alt TEXT NOT NULL, alt TEXT NOT NULL,
created_at INT NOT NULL, created_at INT NOT NULL,