Files
sub-store/internal/handler/share_recycle.go
T

222 lines
6.2 KiB
Go

package handler
import (
"encoding/json"
"strings"
"time"
"github.com/gofiber/fiber/v3"
"github.com/peterqiu0516/sub-store/internal/middleware"
"github.com/peterqiu0516/sub-store/internal/model"
)
// --- Share (download grant) handlers ---
func (d *Deps) HandleListShares(c fiber.Ctx) error {
grants, err := d.GrantRepo.List()
if err != nil {
return failed(c, "Failed to list shares", 500)
}
return success(c, grants)
}
func (d *Deps) HandleCreateShare(c fiber.Ctx) error {
var input map[string]any
if err := json.Unmarshal(c.Body(), &input); err != nil {
return failed(c, "Invalid JSON", 400)
}
resourceType := getStringValue(input["resourceType"])
if resourceType != "source" && resourceType != "collection" {
return failed(c, "Share resourceType and resourceId are required", 400)
}
resourceId := getStringValue(input["resourceId"])
if resourceId == "" {
return failed(c, "Share resourceType and resourceId are required", 400)
}
// Check resource exists
if resourceType == "source" {
s, _ := d.SourceRepo.Get(resourceId)
if s == nil {
return failed(c, "Share resource does not exist", 404)
}
} else {
col, _ := d.CollectionRepo.Get(resourceId)
if col == nil {
return failed(c, "Share resource does not exist", 404)
}
}
target := ""
if input["target"] != nil {
target = model.NormalizeTargetAlias(input["target"])
if input["target"] != "" && target == "" {
return failed(c, "Unsupported target", 400)
}
}
var expiresAt *int64
if v, ok := input["expiresAt"]; ok && v != nil {
if n, ok := v.(float64); ok && n > 0 {
e := int64(n)
expiresAt = &e
}
} else if v, ok := input["expiresIn"]; ok && v != nil {
if n, ok := v.(float64); ok && n > 0 {
e := time.Now().UnixMilli() + int64(n)*1000
expiresAt = &e
}
}
grant, token, err := d.GrantRepo.Create(resourceType, resourceId, target, expiresAt)
if err != nil {
return failed(c, "Failed to create share", 500)
}
// Build download URL
path := "/download/" + resourceType + "/" + resourceId
if target != "" {
path += "/" + target
}
url := getPublicBaseUrl(c) + path + "?token=" + token
return success(c, fiber.Map{
"id": grant.ID,
"resourceType": grant.ResourceType,
"resourceId": grant.ResourceId,
"target": grant.Target,
"expiresAt": grant.ExpiresAt,
"enabled": grant.Enabled,
"createdAt": grant.CreatedAt,
"updatedAt": grant.UpdatedAt,
"token": token,
"url": url,
})
}
func (d *Deps) HandleUpdateShare(c fiber.Ctx) error {
id := c.Params("id")
var input map[string]any
if err := json.Unmarshal(c.Body(), &input); err != nil {
return failed(c, "Invalid JSON", 400)
}
var enabled *bool
if v, ok := input["enabled"].(bool); ok {
enabled = &v
}
var expiresAt *int64
if v, ok := input["expiresAt"]; ok {
if v == nil {
expiresAt = new(int64) // 0 means nil
} else if n, ok := v.(float64); ok {
e := int64(n)
expiresAt = &e
}
}
updated, err := d.GrantRepo.Update(id, enabled, expiresAt)
if err != nil || updated == nil {
return failed(c, "Share not found", 404)
}
return success(c, updated)
}
func (d *Deps) HandleDeleteShare(c fiber.Ctx) error {
id := c.Params("id")
snapshot, err := d.GrantRepo.GetSnapshot(id)
if err != nil || snapshot == nil {
return failed(c, "Share not found", 404)
}
_, err = d.RecycleRepo.ArchiveAndDelete("share", id, snapshot, "DELETE FROM download_grants WHERE id = ?", id)
if err != nil {
return failed(c, "Failed to delete share", 500)
}
return success(c, fiber.Map{"deleted": true})
}
// --- Recycle bin handlers ---
func (d *Deps) HandleListRecycleBin(c fiber.Ctx) error {
entries, err := d.RecycleRepo.List()
if err != nil {
return failed(c, "Failed to list recycle bin", 500)
}
return success(c, entries)
}
func (d *Deps) HandleDeleteRecycleBinEntry(c fiber.Ctx) error {
id := c.Params("id")
entry, err := d.RecycleRepo.Get(id)
if err != nil || entry == nil {
return failed(c, "Recycle entry not found", 404)
}
if err := d.RecycleRepo.Delete(id); err != nil {
return failed(c, "Failed to delete", 500)
}
return success(c, fiber.Map{"deleted": true})
}
func (d *Deps) HandleRestoreRecycleBinEntry(c fiber.Ctx) error {
id := c.Params("id")
entry, err := d.RecycleRepo.Get(id)
if err != nil || entry == nil {
return failed(c, "Recycle entry not found", 404)
}
resourceType := getStringValue(entry["resourceType"])
resourceId := getStringValue(entry["resourceId"])
snapshot := getMapValue(entry["snapshot"])
switch resourceType {
case "source":
existing, _ := d.SourceRepo.Get(resourceId)
if existing != nil {
return failed(c, "Source id already exists", 409)
}
d.SourceRepo.Upsert(mapToSourceRecord(snapshot))
case "collection":
existing, _ := d.CollectionRepo.Get(resourceId)
if existing != nil {
return failed(c, "Collection id already exists", 409)
}
d.CollectionRepo.Upsert(mapToCollectionRecord(snapshot))
case "template":
existing, _ := d.TemplateRepo.Get(resourceId)
if existing != nil {
return failed(c, "Template id already exists", 409)
}
d.TemplateRepo.Upsert(mapToTemplateRecord(snapshot))
case "share":
existing, _ := d.GrantRepo.Get(resourceId)
if existing != nil {
return failed(c, "Share id already exists", 409)
}
// review-resolution #38: restore tokenHash
d.GrantRepo.RestoreFromSnapshot(snapshot)
}
d.RecycleRepo.Delete(id)
return success(c, fiber.Map{
"restored": true,
"resourceType": resourceType,
"resourceId": resourceId,
})
}
// getPublicBaseUrl returns the public base URL for download links.
// Per review-resolution #11: PUBLIC_DOWNLOAD_HOSTS config, fallback to request origin.
func getPublicBaseUrl(c fiber.Ctx) string {
// This is set by the handler using deps config
publicHosts := c.Locals("publicDownloadHosts")
if hosts, ok := publicHosts.(string); ok && hosts != "" {
parts := strings.Split(hosts, ",")
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
return "https://" + p
}
}
}
// Use X-Forwarded-Proto or default to http
proto := c.Get("X-Forwarded-Proto")
if proto == "" {
proto = "http"
}
return proto + "://" + c.Hostname()
}
// SetSafeResponseHeader wraps middleware.SetSafeResponseHeader for handler use.
var SetSafeResponseHeader = middleware.SetSafeResponseHeader