Permanently deleting a photo whose album was deleted failed because authorization resolved through the album. Fall back to uploader ownership when the photo is albumless. Add a bulk DELETE /trash that removes every trashed photo the caller may delete (uncapped listing) and returns the deleted count, with an «Очистить корзину» action on the trash screen.
258 lines
6.6 KiB
Go
258 lines
6.6 KiB
Go
package photos
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"galo/internal/auth"
|
|
"galo/internal/httpx"
|
|
)
|
|
|
|
type Handler struct {
|
|
svc *Service
|
|
}
|
|
|
|
func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} }
|
|
|
|
func (h *Handler) List(c *gin.Context) {
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
|
|
var since *time.Time
|
|
if raw := c.Query("since"); raw != "" {
|
|
if t, err := time.Parse(time.RFC3339, raw); err == nil {
|
|
since = &t
|
|
} else if ms, err := strconv.ParseInt(raw, 10, 64); err == nil {
|
|
t := time.UnixMilli(ms)
|
|
since = &t
|
|
}
|
|
}
|
|
list, next, err := h.svc.List(c.Request.Context(), c.Param("id"), auth.UserID(c), c.Query("cursor"), since, limit)
|
|
if err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"photos": list, "next_cursor": next})
|
|
}
|
|
|
|
func (h *Handler) Upload(c *gin.Context) {
|
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, h.svc.MaxUploadSize())
|
|
form, err := c.MultipartForm()
|
|
if err != nil {
|
|
var maxErr *http.MaxBytesError
|
|
if errors.As(err, &maxErr) {
|
|
httpx.RespondError(c, httpx.NewError(http.StatusRequestEntityTooLarge, "file_too_large", "files exceed the maximum upload size"))
|
|
return
|
|
}
|
|
httpx.RespondError(c, httpx.BadRequest("invalid_request", "invalid multipart form"))
|
|
return
|
|
}
|
|
files := form.File["files"]
|
|
if len(files) == 0 {
|
|
files = form.File["file"]
|
|
}
|
|
if len(files) == 0 {
|
|
httpx.RespondError(c, httpx.BadRequest("no_files", "no files provided"))
|
|
return
|
|
}
|
|
|
|
created := make([]any, 0, len(files))
|
|
for _, fh := range files {
|
|
f, err := fh.Open()
|
|
if err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
p, err := h.svc.Upload(c.Request.Context(), UploadInput{
|
|
AlbumID: c.Param("id"),
|
|
UserID: auth.UserID(c),
|
|
Filename: fh.Filename,
|
|
MimeType: fh.Header.Get("Content-Type"),
|
|
Reader: f,
|
|
Size: fh.Size,
|
|
})
|
|
f.Close()
|
|
if err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
created = append(created, p)
|
|
}
|
|
c.JSON(http.StatusCreated, gin.H{"photos": created})
|
|
}
|
|
|
|
func (h *Handler) Get(c *gin.Context) {
|
|
p, err := h.svc.Get(c.Request.Context(), c.Param("id"), auth.UserID(c))
|
|
if err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
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 {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"url": url})
|
|
}
|
|
|
|
func (h *Handler) Delete(c *gin.Context) {
|
|
if err := h.svc.Delete(c.Request.Context(), c.Param("id"), auth.UserID(c)); err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) PermanentDelete(c *gin.Context) {
|
|
if err := h.svc.PermanentDelete(c.Request.Context(), c.Param("id"), auth.UserID(c)); err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) Restore(c *gin.Context) {
|
|
var req struct {
|
|
AlbumID string `json:"album_id"`
|
|
}
|
|
_ = c.ShouldBindJSON(&req)
|
|
p, err := h.svc.Restore(c.Request.Context(), c.Param("id"), auth.UserID(c), req.AlbumID)
|
|
if err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"photo": p})
|
|
}
|
|
|
|
func (h *Handler) Trash(c *gin.Context) {
|
|
list, err := h.svc.Trash(c.Request.Context(), auth.UserID(c))
|
|
if err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"photos": list})
|
|
}
|
|
|
|
func (h *Handler) TrashCount(c *gin.Context) {
|
|
n, err := h.svc.TrashCount(c.Request.Context(), auth.UserID(c))
|
|
if err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"count": n})
|
|
}
|
|
|
|
func (h *Handler) EmptyTrash(c *gin.Context) {
|
|
n, err := h.svc.EmptyTrash(c.Request.Context(), auth.UserID(c))
|
|
if err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"deleted": n})
|
|
}
|
|
|
|
func (h *Handler) Tags(c *gin.Context) {
|
|
tags, err := h.svc.Tags(c.Request.Context(), auth.UserID(c), c.Query("q"))
|
|
if err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"tags": tags})
|
|
}
|
|
|
|
func (h *Handler) AddTag(c *gin.Context) {
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil || req.Name == "" {
|
|
httpx.RespondError(c, httpx.BadRequest("invalid_request", "name is required"))
|
|
return
|
|
}
|
|
p, err := h.svc.AddTag(c.Request.Context(), c.Param("id"), auth.UserID(c), req.Name)
|
|
if err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"photo": p})
|
|
}
|
|
|
|
func (h *Handler) RemoveTag(c *gin.Context) {
|
|
p, err := h.svc.RemoveTag(c.Request.Context(), c.Param("id"), auth.UserID(c), c.Param("tag_id"))
|
|
if err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"photo": p})
|
|
}
|
|
|
|
type batchRequest struct {
|
|
IDs []string `json:"ids" binding:"required"`
|
|
AlbumID string `json:"album_id"`
|
|
}
|
|
|
|
func (h *Handler) DeleteBatch(c *gin.Context) {
|
|
var req batchRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil || len(req.IDs) == 0 {
|
|
httpx.RespondError(c, httpx.BadRequest("invalid_request", "ids are required"))
|
|
return
|
|
}
|
|
n, err := h.svc.DeleteBatch(c.Request.Context(), req.IDs, auth.UserID(c))
|
|
if err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"deleted": n})
|
|
}
|
|
|
|
func (h *Handler) MoveBatch(c *gin.Context) {
|
|
var req batchRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil || len(req.IDs) == 0 || req.AlbumID == "" {
|
|
httpx.RespondError(c, httpx.BadRequest("invalid_request", "ids and album_id are required"))
|
|
return
|
|
}
|
|
n, err := h.svc.MoveBatch(c.Request.Context(), req.IDs, auth.UserID(c), req.AlbumID)
|
|
if err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"moved": n})
|
|
}
|
|
|
|
func (h *Handler) CopyBatch(c *gin.Context) {
|
|
var req batchRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil || len(req.IDs) == 0 || req.AlbumID == "" {
|
|
httpx.RespondError(c, httpx.BadRequest("invalid_request", "ids and album_id are required"))
|
|
return
|
|
}
|
|
created, err := h.svc.CopyBatch(c.Request.Context(), req.IDs, auth.UserID(c), req.AlbumID)
|
|
if err != nil {
|
|
httpx.RespondError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"photos": created})
|
|
}
|