package imgedit import ( "context" "encoding/json" "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 } // TODO: verify processor, etc. session.Processors = append(session.Processors, models.ImageEditProcessor{ ID: models.NewNanoID(), Type: req.Type, }) 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 } 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 }