feat: photo rename and viewer delete with confirmation
Backend: - add PATCH /api/photos/:id to rename original_filename (uploader/owner/admin) - emit photo.updated SSE event Frontend: - inline filename editor in the right panel - viewer: back button (arrow_back) on the left, delete on the right - delete confirmed via a modal dialog; filmstrip updates after deletion
This commit is contained in:
@@ -133,6 +133,7 @@ func (s *Server) Router() *gin.Engine {
|
||||
authed.GET("/albums/:id/photos", photosHandler.List)
|
||||
authed.POST("/albums/:id/photos", photosHandler.Upload)
|
||||
authed.GET("/photos/:id", photosHandler.Get)
|
||||
authed.PATCH("/photos/:id", photosHandler.Rename)
|
||||
authed.DELETE("/photos/:id", photosHandler.Delete)
|
||||
authed.GET("/photos/:id/download", photosHandler.Download)
|
||||
|
||||
|
||||
@@ -93,6 +93,24 @@ func (h *Handler) Get(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"photo": p})
|
||||
}
|
||||
|
||||
type renameRequest struct {
|
||||
OriginalFilename string `json:"original_filename" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *Handler) Rename(c *gin.Context) {
|
||||
var req renameRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
httpx.RespondError(c, httpx.BadRequest("invalid_request", "original_filename is required"))
|
||||
return
|
||||
}
|
||||
p, err := h.svc.Rename(c.Request.Context(), c.Param("id"), auth.UserID(c), req.OriginalFilename)
|
||||
if err != nil {
|
||||
httpx.RespondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"photo": p})
|
||||
}
|
||||
|
||||
func (h *Handler) Download(c *gin.Context) {
|
||||
url, err := h.svc.Download(c.Request.Context(), c.Param("id"), auth.UserID(c))
|
||||
if err != nil {
|
||||
|
||||
@@ -250,6 +250,40 @@ func (s *Service) Download(ctx context.Context, photoID, userID string) (string,
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func (s *Service) Rename(ctx context.Context, photoID, userID, filename 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
|
||||
}
|
||||
_, 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 rename this photo")
|
||||
}
|
||||
name := strings.TrimSpace(filename)
|
||||
if name == "" {
|
||||
return nil, httpx.BadRequest("invalid_filename", "filename is required")
|
||||
}
|
||||
if len(name) > 255 {
|
||||
name = name[:255]
|
||||
}
|
||||
if err := s.store.UpdateFilename(ctx, photoID, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.OriginalFilename = name
|
||||
if err := s.attachURLs(ctx, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.hub.Publish(p.AlbumID, "photo.updated", p)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, photoID, userID string) error {
|
||||
p, err := s.store.GetByID(ctx, photoID, userID)
|
||||
if err != nil {
|
||||
|
||||
@@ -127,3 +127,14 @@ func (s *Store) SetCoverIfEmpty(ctx context.Context, albumID, photoID string) {
|
||||
`UPDATE albums SET cover_photo_id = $2 WHERE id = $1 AND cover_photo_id IS NULL`,
|
||||
albumID, photoID)
|
||||
}
|
||||
|
||||
func (s *Store) UpdateFilename(ctx context.Context, id, filename string) error {
|
||||
tag, err := s.pool.Exec(ctx, `UPDATE photos SET original_filename = $2 WHERE id = $1`, id, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user