Files
ds-galo/backend/internal/photos/edit_service.go
iu 8dd8cb6f0c fix(photos): копия фото не наследует отредактированную версию
Clone переносил все привязки photo_objects, включая роли edit*, хотя
правка принадлежит фото, чьи правки её породили. Теперь копия делит
только обычные варианты и показывается в исходном виде.

- EditRoles() и IsEditRole() в edit_render.go — единый список ролей;
- фильтр edit-ролей в Store.Clone и SQLiteStore.Clone;
- миграции 000016 (Postgres) и 000008 (SQLite) убирают привязки у
  копий, созданных до фикса;
- контрактная проверка копирования, сервисные и e2e-тесты.
2026-10-21 21:01:33 +03:00

115 lines
3.4 KiB
Go

package photos
import (
"context"
"encoding/json"
"errors"
"galo/internal/httpx"
"galo/internal/models"
"galo/internal/roles"
)
// editablePhoto loads a photo and checks that the user may change its
// editing parameters: the uploader of the photo or an album owner/admin.
func (s *Service) editablePhoto(ctx context.Context, photoID, userID string) (*models.Photo, error) {
p, err := s.store.GetByID(ctx, photoID, userID)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, httpx.NotFound("photo_not_found", "photo not found")
}
return nil, err
}
if p.DeletedAt != nil {
return nil, httpx.NotFound("photo_not_found", "photo not found")
}
_, role, err := s.albums.Access(ctx, p.AlbumID, userID)
if err != nil {
return nil, err
}
own := p.UploaderID == userID
if !(roles.CanDeleteAnyPhoto(role) || (own && roles.CanDeleteOwnPhoto(role))) {
return nil, httpx.Forbidden("forbidden", "you cannot edit this photo")
}
return p, nil
}
// EditPhoto stores new editing parameters for a photo and schedules the
// render of the edited version. The original object is never modified, so the
// edit can always be reset.
func (s *Service) EditPhoto(ctx context.Context, photoID, userID string, params EditParams) (*models.Photo, error) {
p, err := s.editablePhoto(ctx, photoID, userID)
if err != nil {
return nil, err
}
if p.Kind != KindImage {
return nil, httpx.BadRequest("invalid_request", "only images can be edited")
}
params = params.Normalized()
if params.IsEmpty() {
return s.ResetEdit(ctx, photoID, userID)
}
if err := s.store.SetEditParams(ctx, photoID, params.JSON()); err != nil {
return nil, err
}
if err := s.store.EnqueueEditJob(ctx, photoID); err != nil {
return nil, err
}
// Рендер начинается сразу, а не в конце цикла воркера.
if s.wakeEdit != nil {
s.wakeEdit()
}
return s.publishEdit(ctx, photoID, userID)
}
// ResetEdit removes the editing parameters of a photo together with the
// edited version, returning the photo to its original look.
func (s *Service) ResetEdit(ctx context.Context, photoID, userID string) (*models.Photo, error) {
if _, err := s.editablePhoto(ctx, photoID, userID); err != nil {
return nil, err
}
if err := s.store.ClearEditParams(ctx, photoID); err != nil {
return nil, err
}
if err := s.store.DeleteEditJob(ctx, photoID); err != nil {
return nil, err
}
if err := s.store.DeleteObjectBindings(ctx, photoID, EditRoles()...); err != nil {
return nil, err
}
return s.publishEdit(ctx, photoID, userID)
}
// publishEdit reloads a photo with its current editing state and notifies the
// album subscribers.
func (s *Service) publishEdit(ctx context.Context, photoID, userID string) (*models.Photo, error) {
p, err := s.store.GetByID(ctx, photoID, userID)
if err != nil {
if errors.Is(err, ErrNotFound) {
return nil, httpx.NotFound("photo_not_found", "photo not found")
}
return nil, err
}
if err := s.attachURLs(ctx, p); err != nil {
return nil, err
}
s.hub.Publish(p.AlbumID, "photo.updated", p)
return p, nil
}
// EditParamsOf returns the editing parameters stored on a photo.
func EditParamsOf(p *models.Photo) EditParams {
if p == nil || len(p.EditParams) == 0 {
return EditParams{}
}
data, err := json.Marshal(p.EditParams)
if err != nil {
return EditParams{}
}
params, err := ParseEditParams(data)
if err != nil {
return EditParams{}
}
return params
}