feat(backend): implement file handler and wire all /files endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,755 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"tanabata/backend/internal/domain"
|
||||
"tanabata/backend/internal/service"
|
||||
)
|
||||
|
||||
// FileHandler handles all /files endpoints.
|
||||
type FileHandler struct {
|
||||
fileSvc *service.FileService
|
||||
}
|
||||
|
||||
// NewFileHandler creates a FileHandler.
|
||||
func NewFileHandler(fileSvc *service.FileService) *FileHandler {
|
||||
return &FileHandler{fileSvc: fileSvc}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Response types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type tagJSON struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Notes *string `json:"notes"`
|
||||
Color *string `json:"color"`
|
||||
CategoryID *string `json:"category_id"`
|
||||
CategoryName *string `json:"category_name"`
|
||||
CategoryColor *string `json:"category_color"`
|
||||
CreatorID int16 `json:"creator_id"`
|
||||
CreatorName string `json:"creator_name"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type fileJSON struct {
|
||||
ID string `json:"id"`
|
||||
OriginalName *string `json:"original_name"`
|
||||
MIMEType string `json:"mime_type"`
|
||||
MIMEExtension string `json:"mime_extension"`
|
||||
ContentDatetime string `json:"content_datetime"`
|
||||
Notes *string `json:"notes"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
EXIF json.RawMessage `json:"exif"`
|
||||
PHash *int64 `json:"phash"`
|
||||
CreatorID int16 `json:"creator_id"`
|
||||
CreatorName string `json:"creator_name"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Tags []tagJSON `json:"tags"`
|
||||
}
|
||||
|
||||
func toTagJSON(t domain.Tag) tagJSON {
|
||||
j := tagJSON{
|
||||
ID: t.ID.String(),
|
||||
Name: t.Name,
|
||||
Notes: t.Notes,
|
||||
Color: t.Color,
|
||||
CategoryName: t.CategoryName,
|
||||
CategoryColor: t.CategoryColor,
|
||||
CreatorID: t.CreatorID,
|
||||
CreatorName: t.CreatorName,
|
||||
IsPublic: t.IsPublic,
|
||||
CreatedAt: t.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
if t.CategoryID != nil {
|
||||
s := t.CategoryID.String()
|
||||
j.CategoryID = &s
|
||||
}
|
||||
return j
|
||||
}
|
||||
|
||||
func toFileJSON(f domain.File) fileJSON {
|
||||
tags := make([]tagJSON, len(f.Tags))
|
||||
for i, t := range f.Tags {
|
||||
tags[i] = toTagJSON(t)
|
||||
}
|
||||
exif := f.EXIF
|
||||
if exif == nil {
|
||||
exif = json.RawMessage("{}")
|
||||
}
|
||||
return fileJSON{
|
||||
ID: f.ID.String(),
|
||||
OriginalName: f.OriginalName,
|
||||
MIMEType: f.MIMEType,
|
||||
MIMEExtension: f.MIMEExtension,
|
||||
ContentDatetime: f.ContentDatetime.Format(time.RFC3339),
|
||||
Notes: f.Notes,
|
||||
Metadata: f.Metadata,
|
||||
EXIF: exif,
|
||||
PHash: f.PHash,
|
||||
CreatorID: f.CreatorID,
|
||||
CreatorName: f.CreatorName,
|
||||
IsPublic: f.IsPublic,
|
||||
IsDeleted: f.IsDeleted,
|
||||
CreatedAt: f.CreatedAt.Format(time.RFC3339),
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func parseFileID(c *gin.Context) (uuid.UUID, bool) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return uuid.UUID{}, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /files
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) List(c *gin.Context) {
|
||||
params := domain.FileListParams{
|
||||
Cursor: c.Query("cursor"),
|
||||
Direction: c.DefaultQuery("direction", "forward"),
|
||||
Sort: c.DefaultQuery("sort", "created"),
|
||||
Order: c.DefaultQuery("order", "desc"),
|
||||
Filter: c.Query("filter"),
|
||||
Search: c.Query("search"),
|
||||
}
|
||||
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
n, err := strconv.Atoi(limitStr)
|
||||
if err != nil || n < 1 || n > 200 {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
params.Limit = n
|
||||
} else {
|
||||
params.Limit = 50
|
||||
}
|
||||
|
||||
if anchorStr := c.Query("anchor"); anchorStr != "" {
|
||||
id, err := uuid.Parse(anchorStr)
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
params.Anchor = &id
|
||||
}
|
||||
|
||||
if trashStr := c.Query("trash"); trashStr == "true" || trashStr == "1" {
|
||||
params.Trash = true
|
||||
}
|
||||
|
||||
page, err := h.fileSvc.List(c.Request.Context(), params)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]fileJSON, len(page.Items))
|
||||
for i, f := range page.Items {
|
||||
items[i] = toFileJSON(f)
|
||||
}
|
||||
|
||||
respondJSON(c, http.StatusOK, gin.H{
|
||||
"items": items,
|
||||
"next_cursor": page.NextCursor,
|
||||
"prev_cursor": page.PrevCursor,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST /files (multipart upload)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) Upload(c *gin.Context) {
|
||||
fh, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
src, err := fh.Open()
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
// Detect MIME from actual bytes (ignore client-supplied Content-Type).
|
||||
mt, err := mimetype.DetectReader(src)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
// Rewind by reopening — FormFile gives a multipart.File which supports Seek.
|
||||
if _, err := src.Seek(0, io.SeekStart); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
mimeStr := strings.SplitN(mt.String(), ";", 2)[0]
|
||||
|
||||
params := service.UploadParams{
|
||||
Reader: src,
|
||||
MIMEType: mimeStr,
|
||||
IsPublic: c.PostForm("is_public") == "true",
|
||||
}
|
||||
|
||||
if name := fh.Filename; name != "" {
|
||||
params.OriginalName = &name
|
||||
}
|
||||
if notes := c.PostForm("notes"); notes != "" {
|
||||
params.Notes = ¬es
|
||||
}
|
||||
if metaStr := c.PostForm("metadata"); metaStr != "" {
|
||||
params.Metadata = json.RawMessage(metaStr)
|
||||
}
|
||||
if dtStr := c.PostForm("content_datetime"); dtStr != "" {
|
||||
t, err := time.Parse(time.RFC3339, dtStr)
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
params.ContentDatetime = &t
|
||||
}
|
||||
if tagIDsStr := c.PostForm("tag_ids"); tagIDsStr != "" {
|
||||
for _, raw := range strings.Split(tagIDsStr, ",") {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
id, err := uuid.Parse(raw)
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
params.TagIDs = append(params.TagIDs, id)
|
||||
}
|
||||
}
|
||||
|
||||
f, err := h.fileSvc.Upload(c.Request.Context(), params)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(c, http.StatusCreated, toFileJSON(*f))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /files/:id
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) GetMeta(c *gin.Context) {
|
||||
id, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := h.fileSvc.Get(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(c, http.StatusOK, toFileJSON(*f))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PATCH /files/:id
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) UpdateMeta(c *gin.Context) {
|
||||
id, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
OriginalName *string `json:"original_name"`
|
||||
ContentDatetime *string `json:"content_datetime"`
|
||||
Notes *string `json:"notes"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
IsPublic *bool `json:"is_public"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
params := service.UpdateParams{
|
||||
OriginalName: body.OriginalName,
|
||||
Notes: body.Notes,
|
||||
Metadata: body.Metadata,
|
||||
IsPublic: body.IsPublic,
|
||||
}
|
||||
if body.ContentDatetime != nil {
|
||||
t, err := time.Parse(time.RFC3339, *body.ContentDatetime)
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
params.ContentDatetime = &t
|
||||
}
|
||||
|
||||
f, err := h.fileSvc.Update(c.Request.Context(), id, params)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(c, http.StatusOK, toFileJSON(*f))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DELETE /files/:id (soft-delete)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) SoftDelete(c *gin.Context) {
|
||||
id, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.fileSvc.Delete(c.Request.Context(), id); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /files/:id/content
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) GetContent(c *gin.Context) {
|
||||
id, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := h.fileSvc.GetContent(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
c.Header("Content-Type", res.MIMEType)
|
||||
if res.OriginalName != nil {
|
||||
c.Header("Content-Disposition",
|
||||
fmt.Sprintf("attachment; filename=%q", *res.OriginalName))
|
||||
}
|
||||
c.Status(http.StatusOK)
|
||||
io.Copy(c.Writer, res.Body) //nolint:errcheck
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PUT /files/:id/content (replace)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) ReplaceContent(c *gin.Context) {
|
||||
id, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
fh, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
src, err := fh.Open()
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
mt, err := mimetype.DetectReader(src)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
if _, err := src.Seek(0, io.SeekStart); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
mimeStr := strings.SplitN(mt.String(), ";", 2)[0]
|
||||
|
||||
name := fh.Filename
|
||||
params := service.UploadParams{
|
||||
Reader: src,
|
||||
MIMEType: mimeStr,
|
||||
OriginalName: &name,
|
||||
}
|
||||
|
||||
f, err := h.fileSvc.Replace(c.Request.Context(), id, params)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(c, http.StatusOK, toFileJSON(*f))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /files/:id/thumbnail
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) GetThumbnail(c *gin.Context) {
|
||||
id, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
rc, err := h.fileSvc.GetThumbnail(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
c.Header("Content-Type", "image/jpeg")
|
||||
c.Status(http.StatusOK)
|
||||
io.Copy(c.Writer, rc) //nolint:errcheck
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /files/:id/preview
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) GetPreview(c *gin.Context) {
|
||||
id, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
rc, err := h.fileSvc.GetPreview(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
c.Header("Content-Type", "image/jpeg")
|
||||
c.Status(http.StatusOK)
|
||||
io.Copy(c.Writer, rc) //nolint:errcheck
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST /files/:id/restore
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) Restore(c *gin.Context) {
|
||||
id, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := h.fileSvc.Restore(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(c, http.StatusOK, toFileJSON(*f))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DELETE /files/:id/permanent
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) PermanentDelete(c *gin.Context) {
|
||||
id, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.fileSvc.PermanentDelete(c.Request.Context(), id); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /files/:id/tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) ListTags(c *gin.Context) {
|
||||
id, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := h.fileSvc.ListFileTags(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]tagJSON, len(tags))
|
||||
for i, t := range tags {
|
||||
items[i] = toTagJSON(t)
|
||||
}
|
||||
respondJSON(c, http.StatusOK, items)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PUT /files/:id/tags (replace all)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) SetTags(c *gin.Context) {
|
||||
id, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
TagIDs []string `json:"tag_ids" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
tagIDs, err := parseUUIDs(body.TagIDs)
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := h.fileSvc.SetFileTags(c.Request.Context(), id, tagIDs)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]tagJSON, len(tags))
|
||||
for i, t := range tags {
|
||||
items[i] = toTagJSON(t)
|
||||
}
|
||||
respondJSON(c, http.StatusOK, items)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PUT /files/:id/tags/:tag_id
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) AddTag(c *gin.Context) {
|
||||
fileID, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
tagID, err := uuid.Parse(c.Param("tag_id"))
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
tags, err := h.fileSvc.AddTag(c.Request.Context(), fileID, tagID)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]tagJSON, len(tags))
|
||||
for i, t := range tags {
|
||||
items[i] = toTagJSON(t)
|
||||
}
|
||||
respondJSON(c, http.StatusOK, items)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DELETE /files/:id/tags/:tag_id
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) RemoveTag(c *gin.Context) {
|
||||
fileID, ok := parseFileID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
tagID, err := uuid.Parse(c.Param("tag_id"))
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.fileSvc.RemoveTag(c.Request.Context(), fileID, tagID); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST /files/bulk/tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) BulkSetTags(c *gin.Context) {
|
||||
var body struct {
|
||||
FileIDs []string `json:"file_ids" binding:"required"`
|
||||
Action string `json:"action" binding:"required"`
|
||||
TagIDs []string `json:"tag_ids" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
if body.Action != "add" && body.Action != "remove" {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
fileIDs, err := parseUUIDs(body.FileIDs)
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
tagIDs, err := parseUUIDs(body.TagIDs)
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
applied, err := h.fileSvc.BulkSetTags(c.Request.Context(), fileIDs, body.Action, tagIDs)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
strs := make([]string, len(applied))
|
||||
for i, id := range applied {
|
||||
strs[i] = id.String()
|
||||
}
|
||||
respondJSON(c, http.StatusOK, gin.H{"applied_tag_ids": strs})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST /files/bulk/delete
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) BulkDelete(c *gin.Context) {
|
||||
var body struct {
|
||||
FileIDs []string `json:"file_ids" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
fileIDs, err := parseUUIDs(body.FileIDs)
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.fileSvc.BulkDelete(c.Request.Context(), fileIDs); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST /files/bulk/common-tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) CommonTags(c *gin.Context) {
|
||||
var body struct {
|
||||
FileIDs []string `json:"file_ids" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
fileIDs, err := parseUUIDs(body.FileIDs)
|
||||
if err != nil {
|
||||
respondError(c, domain.ErrValidation)
|
||||
return
|
||||
}
|
||||
|
||||
common, partial, err := h.fileSvc.CommonTags(c.Request.Context(), fileIDs)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
toStrs := func(ids []uuid.UUID) []string {
|
||||
s := make([]string, len(ids))
|
||||
for i, id := range ids {
|
||||
s[i] = id.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
respondJSON(c, http.StatusOK, gin.H{
|
||||
"common_tag_ids": toStrs(common),
|
||||
"partial_tag_ids": toStrs(partial),
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST /files/import
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (h *FileHandler) Import(c *gin.Context) {
|
||||
var body struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
// Body is optional; ignore bind errors.
|
||||
_ = c.ShouldBindJSON(&body)
|
||||
|
||||
result, err := h.fileSvc.Import(c.Request.Context(), body.Path)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(c, http.StatusOK, result)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func parseUUIDs(strs []string) ([]uuid.UUID, error) {
|
||||
ids := make([]uuid.UUID, 0, len(strs))
|
||||
for _, s := range strs {
|
||||
id, err := uuid.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
Reference in New Issue
Block a user