package controllers import ( "math" "mime" "path/filepath" "strconv" "strings" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" "github.com/sujit-baniya/fiber-boilerplate/pkg/services" ) // helper: obtiene el provider OSS por ID o el activo por defecto func getOSSProvider(c *fiber.Ctx) (services.OSSProvider, error) { idStr := c.Query("oss_api_id", "") if idStr == "" { idStr = c.FormValue("oss_api_id", "") } if idStr != "" { id, err := strconv.ParseUint(idStr, 10, 32) if err == nil && id > 0 { return services.NewOSSProviderByID(uint(id)) } } return services.NewOSSProviderFromDB() } // OssApiIndex renderiza la vista del panel de gestión de Alibaba Cloud OSS. func OssApiIndex(c *fiber.Ctx) error { data := fiber.Map{ "user": c.Locals("user").(map[string]interface{}), "modules": c.Locals("userModules"), } return c.Render("oss_api", data, "layouts/main") } // GetOssApiConfigs devuelve la lista paginada de configuraciones en JSON. func GetOssApiConfigs(c *fiber.Ctx) error { page, _ := strconv.Atoi(c.Query("page", "1")) if page < 1 { page = 1 } limit := 20 offset := (page - 1) * limit search := c.Query("search", "") items, total, err := models.GetAllOssApi(limit, offset, search) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "items": items, "total": total, "totalPages": int(math.Ceil(float64(total) / float64(limit))), "page": page, "limit": limit, }) } // CreateOssApiConfig crea una nueva configuración de Alibaba Cloud OSS. func CreateOssApiConfig(c *fiber.Ctx) error { type Req struct { Name string `json:"name"` Provider string `json:"provider"` Endpoint string `json:"endpoint"` AccessKeyID string `json:"access_key_id"` AccessKeySecret string `json:"access_key_secret"` BucketName string `json:"bucket_name"` Region string `json:"region"` IsActive bool `json:"is_active"` Notes string `json:"notes"` } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"}) } if strings.TrimSpace(req.Name) == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name es requerido"}) } if strings.TrimSpace(req.Endpoint) == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "endpoint es requerido"}) } if strings.TrimSpace(req.AccessKeyID) == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "access_key_id es requerido"}) } if strings.TrimSpace(req.AccessKeySecret) == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "access_key_secret es requerido"}) } if strings.TrimSpace(req.BucketName) == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bucket_name es requerido"}) } provider := strings.TrimSpace(req.Provider) if provider == "" { provider = "alibaba" } item := models.OssApi{ Name: strings.TrimSpace(req.Name), Provider: provider, Endpoint: strings.TrimSpace(req.Endpoint), AccessKeyID: strings.TrimSpace(req.AccessKeyID), AccessKeySecret: strings.TrimSpace(req.AccessKeySecret), BucketName: strings.TrimSpace(req.BucketName), Region: strings.TrimSpace(req.Region), IsActive: req.IsActive, Notes: strings.TrimSpace(req.Notes), } if err := models.CreateOssApi(&item); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.Status(fiber.StatusCreated).JSON(fiber.Map{"ok": true, "item": item}) } // UpdateOssApiConfig actualiza una configuración existente de Alibaba Cloud OSS. func UpdateOssApiConfig(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"}) } existing, err := models.GetOssApiByID(uint(id)) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "configuración no encontrada"}) } type Req struct { Name string `json:"name"` Provider string `json:"provider"` Endpoint string `json:"endpoint"` AccessKeyID string `json:"access_key_id"` AccessKeySecret string `json:"access_key_secret"` BucketName string `json:"bucket_name"` Region string `json:"region"` IsActive bool `json:"is_active"` Notes string `json:"notes"` } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body inválido"}) } if strings.TrimSpace(req.Name) == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name es requerido"}) } existing.Name = strings.TrimSpace(req.Name) existing.Provider = strings.TrimSpace(req.Provider) existing.Endpoint = strings.TrimSpace(req.Endpoint) existing.BucketName = strings.TrimSpace(req.BucketName) existing.Region = strings.TrimSpace(req.Region) existing.IsActive = req.IsActive existing.Notes = strings.TrimSpace(req.Notes) // Solo actualizar secrets si se envían valores no vacíos if strings.TrimSpace(req.AccessKeyID) != "" { existing.AccessKeyID = strings.TrimSpace(req.AccessKeyID) } if strings.TrimSpace(req.AccessKeySecret) != "" { existing.AccessKeySecret = strings.TrimSpace(req.AccessKeySecret) } if err := models.UpdateOssApi(existing); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true, "item": existing}) } // GetActiveOssApiList devuelve todas las configuraciones activas (para selectores UI). func GetActiveOssApiList(c *fiber.Ctx) error { items, err := models.GetActiveOssApis() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"data": items}) } // DeleteOssApiConfig elimina una configuración de Alibaba Cloud OSS. func DeleteOssApiConfig(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"}) } item, err := models.GetOssApiByID(uint(id)) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "configuración no encontrada"}) } if err := models.DeleteOssApi(item); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // ─── Explorador de archivos OSS ────────────────────────────────────────────── // OssBrowserList lista los objetos del bucket activo con soporte de carpetas virtuales. // Query params: prefix (carpeta actual), marker (paginación), max_keys func OssBrowserList(c *fiber.Ctx) error { svc, err := getOSSProvider(c) if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Sin configuración OSS activa: " + err.Error()}) } prefix := c.Query("prefix", "") marker := c.Query("marker", "") maxKeys, _ := strconv.Atoi(c.Query("max_keys", "100")) result, err := svc.ListObjects(prefix, marker, maxKeys) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(result) } // OssBrowserSignedURL genera una URL firmada temporal (1 hora) para previsualizar o descargar un objeto. // Query param: key (objeto) func OssBrowserSignedURL(c *fiber.Ctx) error { key := strings.TrimSpace(c.Query("key", "")) if key == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "key es requerido"}) } svc, err := getOSSProvider(c) if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Sin configuración OSS activa: " + err.Error()}) } url, err := svc.SignedURL(key, 3600) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"url": url}) } // OssBrowserDelete elimina un objeto del bucket activo. // Body JSON: { "key": "ruta/al/archivo.jpg", "oss_api_id": 1 } func OssBrowserDelete(c *fiber.Ctx) error { type Req struct { Key string `json:"key"` } var req Req if err := c.BodyParser(&req); err != nil || strings.TrimSpace(req.Key) == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "key es requerido"}) } svc, err := getOSSProvider(c) if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Sin configuración OSS activa: " + err.Error()}) } if err := svc.DeleteFile(req.Key); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // OssBrowserUpload sube un archivo al bucket activo. // Form field: file (multipart), prefix (carpeta destino opcional) func OssBrowserUpload(c *fiber.Ctx) error { prefix := strings.TrimSpace(c.FormValue("prefix", "")) fh, err := c.FormFile("file") if err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "archivo requerido"}) } f, err := fh.Open() if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "error abriendo archivo"}) } defer f.Close() objectKey := fh.Filename if prefix != "" { objectKey = strings.TrimRight(prefix, "/") + "/" + fh.Filename } ct := mime.TypeByExtension(filepath.Ext(fh.Filename)) if ct == "" { ct = "application/octet-stream" } svc, err := getOSSProvider(c) if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "Sin configuración OSS activa: " + err.Error()}) } if err := svc.UploadFromReader(objectKey, ct, f); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true, "key": objectKey}) }