fix(vcard): decir por qué falla la subida a OSS, y no armar la clave con texto crudo del cliente
El sistema que consume /v2/vcard/qr-url solo recibía "Error subiendo archivo a OSS": el motivo quedaba en nuestro log y del otro lado no había nada que hacer con eso. Ahora la respuesta incluye el error real del proveedor —es una API entre servidores y el que llama ya conoce la config de OSS que mandó— y el log dice además qué clave se intentó subir. La clave del objeto se armaba con el nombre tal como venía en el body. Un nombre con "/" o con caracteres que OSS no acepta producía justamente ese error genérico. Ahora se limpia. El mismo nombre crudo se usaba para el archivo temporal (/tmp/<nombre>.webp), o sea escritura de archivos en una ruta que elegía quien llamaba. Se fue entero: los tres endpoints suben desde memoria con UploadFromReader, que además saca el paso a disco y su limpieza. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4674413257
commit
17ed57cb56
@@ -2,6 +2,7 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
@@ -38,3 +39,30 @@ func getOSSFromQuery(c *fiber.Ctx) (services.OSSProvider, error) {
|
|||||||
}
|
}
|
||||||
return services.NewOSSProvider(lastActive)
|
return services.NewOSSProvider(lastActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// claveObjetoSegura arma una clave de objeto a partir de texto que viene del
|
||||||
|
// cliente. Sin esto, un nombre con "/" o con caracteres de control produce una
|
||||||
|
// clave que OSS rechaza — y el que llama solo ve "error subiendo a OSS".
|
||||||
|
func claveObjetoSegura(nombre string) string {
|
||||||
|
nombre = strings.TrimSpace(nombre)
|
||||||
|
limpio := strings.Map(func(r rune) rune {
|
||||||
|
switch {
|
||||||
|
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9':
|
||||||
|
return r
|
||||||
|
case r == '-', r == '_', r == '.':
|
||||||
|
return r
|
||||||
|
case r == ' ':
|
||||||
|
return '_'
|
||||||
|
default:
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}, nombre)
|
||||||
|
limpio = strings.Trim(limpio, "._-")
|
||||||
|
if limpio == "" {
|
||||||
|
return "sin-nombre"
|
||||||
|
}
|
||||||
|
if len(limpio) > 80 {
|
||||||
|
limpio = limpio[:80]
|
||||||
|
}
|
||||||
|
return limpio
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// La clave de objeto se arma con texto que manda el cliente. Antes iba tal
|
||||||
|
// cual: un nombre con "/" o con acentos raros producía una clave que OSS
|
||||||
|
// rechaza, y del otro lado solo se veía "error subiendo archivo a OSS".
|
||||||
|
func TestClaveObjetoSegura(t *testing.T) {
|
||||||
|
casos := map[string]string{
|
||||||
|
"Juan Pérez": "Juan_Prez",
|
||||||
|
"Ana/María": "AnaMara",
|
||||||
|
"../../etc/pass": "etcpass",
|
||||||
|
" ": "sin-nombre",
|
||||||
|
"": "sin-nombre",
|
||||||
|
"ok-nombre_1.2": "ok-nombre_1.2",
|
||||||
|
"emoji 🚀 fin": "emoji__fin",
|
||||||
|
}
|
||||||
|
for in, want := range casos {
|
||||||
|
if got := claveObjetoSegura(in); got != want {
|
||||||
|
t.Errorf("claveObjetoSegura(%q) = %q, want %q", in, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := claveObjetoSegura(string(make([]byte, 0)) + "a" + string(rune(0))); got != "a" {
|
||||||
|
t.Errorf("caracter de control no filtrado: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image/png"
|
"image/png"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/chai2010/webp"
|
"github.com/chai2010/webp"
|
||||||
@@ -53,29 +52,24 @@ func CreateQr(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
random := helpers.RandomString(4)
|
random := helpers.RandomString(4)
|
||||||
fileName := fmt.Sprintf("qrs/qr-%s-%s.webp", strings.ReplaceAll(data.FirstName, " ", "_"), random)
|
fileName := fmt.Sprintf("qrs/qr-%s-%s.webp", claveObjetoSegura(data.FirstName), random)
|
||||||
tmpPath := fmt.Sprintf("/tmp/%s.webp", data.FirstName)
|
|
||||||
|
|
||||||
// 4. Guardar archivo temporal
|
// 4. Subir a OSS directo desde memoria: el archivo temporal solo agregaba
|
||||||
if err := os.WriteFile(tmpPath, webpBuf.Bytes(), 0644); err != nil {
|
// una forma más de fallar (y el nombre venía del cliente, sin limpiar).
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
||||||
"error": "No se pudo guardar el QR temporalmente",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
defer os.Remove(tmpPath)
|
|
||||||
|
|
||||||
// 5. Subir a OSS
|
|
||||||
ossProvider, err := getOSSFromBody(c)
|
ossProvider, err := getOSSFromBody(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[QR] No se pudo resolver la config de OSS: %v", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
"error": "Error de conexión con OSS",
|
"error": "Error de conexión con OSS", "detalle": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ossProvider.UploadFile(fileName, tmpPath); err != nil {
|
if err := ossProvider.UploadFromReader(fileName, "image/webp", bytes.NewReader(webpBuf.Bytes())); err != nil {
|
||||||
log.Printf("Error subiendo archivo a OSS: %v", err)
|
log.Printf("Error subiendo archivo a OSS (%s): %v", fileName, err)
|
||||||
|
// El detalle viaja al que llama: es una API entre servidores y sin esto
|
||||||
|
// el otro lado solo ve "error subiendo a OSS" y no puede hacer nada.
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
"error": "Error subiendo archivo a OSS",
|
"error": "Error subiendo archivo a OSS", "detalle": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +166,7 @@ func CreateQrTmp(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
func CreateUrlQr(c *fiber.Ctx) error {
|
func CreateUrlQr(c *fiber.Ctx) error {
|
||||||
var data struct {
|
var data struct {
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Nombre string `json:"unico"`
|
Nombre string `json:"unico"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(c.Body(), &data); err != nil {
|
if err := json.Unmarshal(c.Body(), &data); err != nil {
|
||||||
@@ -187,14 +181,14 @@ func CreateUrlQr(c *fiber.Ctx) error {
|
|||||||
"error": "No se pudo generar el QR",
|
"error": "No se pudo generar el QR",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
img, err := png.Decode(bytes.NewReader(qrBytes))
|
img, err := png.Decode(bytes.NewReader(qrBytes))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
"error": "No se pudo procesar la imagen",
|
"error": "No se pudo procesar la imagen",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Codificar a WebP
|
// Codificar a WebP
|
||||||
var webpBuffer bytes.Buffer
|
var webpBuffer bytes.Buffer
|
||||||
if err := webp.Encode(&webpBuffer, img, &webp.Options{Lossless: true}); err != nil {
|
if err := webp.Encode(&webpBuffer, img, &webp.Options{Lossless: true}); err != nil {
|
||||||
@@ -202,7 +196,7 @@ func CreateUrlQr(c *fiber.Ctx) error {
|
|||||||
"error": "No se pudo convertir a WebP",
|
"error": "No se pudo convertir a WebP",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Convertir a WebP
|
// 3. Convertir a WebP
|
||||||
var webpBuf bytes.Buffer
|
var webpBuf bytes.Buffer
|
||||||
if err := webp.Encode(&webpBuf, img, nil); err != nil {
|
if err := webp.Encode(&webpBuf, img, nil); err != nil {
|
||||||
@@ -211,40 +205,29 @@ func CreateUrlQr(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileName := fmt.Sprintf("qrs/url/qr-%s.webp", claveObjetoSegura(data.Nombre))
|
||||||
|
|
||||||
fileName := fmt.Sprintf("qrs/url/qr-%s.webp", strings.ReplaceAll(data.Nombre, " ", "_") )
|
// 4. Subir a OSS directo desde memoria (ver el comentario en CreateQr).
|
||||||
tmpPath := fmt.Sprintf("/tmp/%s.webp", data.Nombre)
|
|
||||||
|
|
||||||
// 4. Guardar archivo temporal
|
|
||||||
if err := os.WriteFile(tmpPath, webpBuf.Bytes(), 0644); err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
||||||
"error": "No se pudo guardar el QR temporalmente",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
defer os.Remove(tmpPath)
|
|
||||||
|
|
||||||
// 5. Subir a OSS
|
|
||||||
ossProvider, err := getOSSFromBody(c)
|
ossProvider, err := getOSSFromBody(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[QR URL] No se pudo resolver la config de OSS: %v", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
"error": "Error de conexión con OSS",
|
"error": "Error de conexión con OSS", "detalle": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ossProvider.UploadFile(fileName, tmpPath); err != nil {
|
if err := ossProvider.UploadFromReader(fileName, "image/webp", bytes.NewReader(webpBuf.Bytes())); err != nil {
|
||||||
log.Printf("Error subiendo archivo a OSS: %v", err)
|
log.Printf("Error subiendo archivo a OSS (%s): %v", fileName, err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
"error": "Error subiendo archivo a OSS",
|
"error": "Error subiendo archivo a OSS", "detalle": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. URL del archivo
|
// 6. URL del archivo
|
||||||
url := ossProvider.PublicURL(fileName)
|
url := ossProvider.PublicURL(fileName)
|
||||||
|
|
||||||
|
|
||||||
// 8. Retornar la URL
|
// 8. Retornar la URL
|
||||||
return c.JSON(fiber.Map{
|
return c.JSON(fiber.Map{
|
||||||
"url": url,
|
"url": url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||||
@@ -34,29 +33,21 @@ func CreateVcf(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
random := helpers.RandomString(4)
|
random := helpers.RandomString(4)
|
||||||
fileName := fmt.Sprintf("vcf/vcf-%s-%s.vcf", strings.ReplaceAll(data.FirstName, " ", "_"), random)
|
fileName := fmt.Sprintf("vcf/vcf-%s-%s.vcf", claveObjetoSegura(data.FirstName), random)
|
||||||
tmpPath := fmt.Sprintf("/tmp/vcf-%s-%s.vcf", strings.ReplaceAll(data.FirstName, " ", "_"), random)
|
|
||||||
|
|
||||||
// 2. Guardar archivo temporal
|
// 2. Subir a OSS directo desde memoria (ver el comentario en CreateQr).
|
||||||
if err := os.WriteFile(tmpPath, vcfContent, 0644); err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
||||||
"error": "No se pudo guardar el VCF temporalmente",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
defer os.Remove(tmpPath)
|
|
||||||
|
|
||||||
// 3. Subir a OSS
|
|
||||||
ossProvider, err := getOSSFromBody(c)
|
ossProvider, err := getOSSFromBody(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[VCF] No se pudo resolver la config de OSS: %v", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
"error": "Error de conexión con OSS",
|
"error": "Error de conexión con OSS", "detalle": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ossProvider.UploadFile(fileName, tmpPath); err != nil {
|
if err := ossProvider.UploadFromReader(fileName, "text/vcard; charset=utf-8", bytes.NewReader(vcfContent)); err != nil {
|
||||||
log.Printf("Error subiendo archivo a OSS: %v", err)
|
log.Printf("Error subiendo archivo a OSS (%s): %v", fileName, err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||||
"error": "Error subiendo archivo a OSS",
|
"error": "Error subiendo archivo a OSS", "detalle": err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user