267 lines
6.1 KiB
Go
267 lines
6.1 KiB
Go
package imgedit
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"lmika.dev/lmika/weiro/models"
|
|
"lmika.dev/lmika/weiro/services/uploads"
|
|
"lmika.dev/pkg/modash/moslice"
|
|
)
|
|
|
|
type Service struct {
|
|
scratchDir string
|
|
uploadService *uploads.Service
|
|
sessionStore *sessionStore
|
|
}
|
|
|
|
func New(
|
|
uploadService *uploads.Service,
|
|
scratchDir string,
|
|
) *Service {
|
|
return &Service{
|
|
scratchDir: scratchDir,
|
|
uploadService: uploadService,
|
|
sessionStore: &sessionStore{baseDir: scratchDir},
|
|
}
|
|
}
|
|
|
|
func (s *Service) NewSession(ctx context.Context, baseUploadID int64) (*models.ImageEditSession, error) {
|
|
site, user, err := s.fetchSiteAndUser(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
upload, _, err := s.uploadService.OpenUpload(ctx, baseUploadID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var ext string
|
|
switch upload.MIMEType {
|
|
case "image/jpeg":
|
|
ext = "jpg"
|
|
case "image/png":
|
|
ext = "png"
|
|
default:
|
|
return nil, models.UnsupportedImageFormat
|
|
}
|
|
|
|
newSession := models.ImageEditSession{
|
|
GUID: models.NewNanoID(),
|
|
SiteID: site.ID,
|
|
UserID: user.ID,
|
|
BaseUploadID: baseUploadID,
|
|
ImageExt: ext,
|
|
CreatedAt: time.Now().UTC(),
|
|
UpdatedAt: time.Now().UTC(),
|
|
Processors: []models.ImageEditProcessor{
|
|
{
|
|
ID: models.NewNanoID(),
|
|
Type: "copy-upload",
|
|
Props: mustToJSON(models.CopyUploadProps{UploadID: baseUploadID}),
|
|
},
|
|
},
|
|
}
|
|
|
|
newSession.RecalcVersionIDs()
|
|
if err := s.sessionStore.save(&newSession); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := s.reprocess(ctx, &newSession); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &newSession, nil
|
|
}
|
|
|
|
func (s *Service) LoadImageVersion(ctx context.Context, sessionID string, versionID string) (mimeType string, rw func() (io.ReadCloser, error), err error) {
|
|
session, err := s.loadAndVerifySession(ctx, sessionID)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
return s.sessionStore.getImage(session, versionID+"."+session.ImageExt)
|
|
}
|
|
|
|
type AddProcessorReq struct {
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
func (s *Service) AddProcessor(ctx context.Context, sessionID string, req AddProcessorReq) (*models.ImageEditSession, error) {
|
|
session, err := s.loadAndVerifySession(ctx, sessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
proc, ok := processors[req.Type]
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown processor type: %v", req.Type)
|
|
}
|
|
|
|
paramType := proc.newParams()
|
|
paramBytes, err := json.Marshal(paramType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
session.Processors = append(session.Processors, models.ImageEditProcessor{
|
|
ID: models.NewNanoID(),
|
|
Type: req.Type,
|
|
Props: paramBytes,
|
|
})
|
|
|
|
session.RecalcVersionIDs()
|
|
if err := s.sessionStore.save(session); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := s.reprocess(ctx, session); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return session, nil
|
|
}
|
|
|
|
func (s *Service) DeleteProcessor(ctx context.Context, sessionID, processorID string) (*models.ImageEditSession, error) {
|
|
session, err := s.loadAndVerifySession(ctx, sessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
session.Processors = moslice.Filter(session.Processors, func(p models.ImageEditProcessor) bool { return p.ID != processorID })
|
|
session.RecalcVersionIDs()
|
|
if err := s.sessionStore.save(session); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := s.reprocess(ctx, session); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return session, nil
|
|
}
|
|
|
|
type UpdateProcessorReq struct {
|
|
ID string `json:"id"`
|
|
Props json.RawMessage `json:"props"`
|
|
}
|
|
|
|
func (s *Service) UpdateProcessor(ctx context.Context, sessionID string, req UpdateProcessorReq) (*models.ImageEditSession, error) {
|
|
session, err := s.loadAndVerifySession(ctx, sessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i, p := range session.Processors {
|
|
if p.ID == req.ID {
|
|
session.Processors[i].Props = req.Props
|
|
break
|
|
}
|
|
}
|
|
|
|
session.RecalcVersionIDs()
|
|
if err := s.sessionStore.save(session); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := s.reprocess(ctx, session); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return session, nil
|
|
}
|
|
|
|
type SaveResult struct {
|
|
UploadID int64 `json:"upload_id"`
|
|
}
|
|
|
|
func (s *Service) Save(ctx context.Context, sessionID string, mode string) (*SaveResult, error) {
|
|
session, err := s.loadAndVerifySession(ctx, sessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(session.Processors) == 0 {
|
|
return nil, fmt.Errorf("no processors in session")
|
|
}
|
|
|
|
lastProc := session.Processors[len(session.Processors)-1]
|
|
finalImagePath := fmt.Sprintf("%v/%v/%v.%v", s.scratchDir, session.GUID, lastProc.VersionID, session.ImageExt)
|
|
|
|
var mimeType string
|
|
switch session.ImageExt {
|
|
case "jpg", "jpeg":
|
|
mimeType = "image/jpeg"
|
|
case "png":
|
|
mimeType = "image/png"
|
|
}
|
|
|
|
var uploadID int64
|
|
switch mode {
|
|
case "replace":
|
|
upload, err := s.uploadService.ReplaceUploadFile(ctx, session.BaseUploadID, finalImagePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
uploadID = upload.ID
|
|
case "copy":
|
|
baseUpload, _, err := s.uploadService.OpenUpload(ctx, session.BaseUploadID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
upload, err := s.uploadService.CreateUploadFromFile(ctx, finalImagePath, baseUpload.Filename, mimeType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
uploadID = upload.ID
|
|
default:
|
|
return nil, fmt.Errorf("unknown save mode: %v", mode)
|
|
}
|
|
|
|
s.sessionStore.delete(session.GUID)
|
|
|
|
return &SaveResult{UploadID: uploadID}, nil
|
|
}
|
|
|
|
func (s *Service) loadAndVerifySession(ctx context.Context, sessionID string) (*models.ImageEditSession, error) {
|
|
site, user, err := s.fetchSiteAndUser(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
session, err := s.sessionStore.get(sessionID)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if session.SiteID != site.ID || session.UserID != user.ID {
|
|
return nil, models.PermissionError
|
|
}
|
|
return session, nil
|
|
}
|
|
|
|
func (s *Service) fetchSiteAndUser(ctx context.Context) (models.Site, models.User, error) {
|
|
user, ok := models.GetUser(ctx)
|
|
if !ok {
|
|
return models.Site{}, models.User{}, models.UserRequiredError
|
|
}
|
|
|
|
site, ok := models.GetSite(ctx)
|
|
if !ok {
|
|
return models.Site{}, models.User{}, models.SiteRequiredError
|
|
}
|
|
|
|
if site.OwnerID != user.ID {
|
|
return models.Site{}, models.User{}, models.PermissionError
|
|
}
|
|
|
|
return site, user, nil
|
|
}
|
|
|
|
func mustToJSON(a any) json.RawMessage {
|
|
b, _ := json.Marshal(a)
|
|
return b
|
|
}
|