207 lines
6.0 KiB
Go
207 lines
6.0 KiB
Go
package controllers
|
|
|
|
import (
|
|
"fmt"
|
|
"mime/multipart"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
)
|
|
|
|
// GetClienteDocumentos devuelve todos los documentos de un cliente.
|
|
func GetClienteDocumentos(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseUint(c.Params("clienteID"), 10, 32)
|
|
if err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
|
}
|
|
docs, err := models.GetDocumentosByCliente(uint(id))
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(docs)
|
|
}
|
|
|
|
// UploadClienteDocumentos recibe uno o más archivos (campo "archivos") y un campo
|
|
// opcional "fecha_expedicion" (YYYY-MM-DD) por cada archivo o uno compartido.
|
|
func UploadClienteDocumentos(c *fiber.Ctx) error {
|
|
id, err := strconv.ParseUint(c.Params("clienteID"), 10, 32)
|
|
if err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
|
}
|
|
|
|
form, err := c.MultipartForm()
|
|
if err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "Formulario inválido: " + err.Error()})
|
|
}
|
|
|
|
files := form.File["archivos"]
|
|
if len(files) == 0 {
|
|
return c.Status(400).JSON(fiber.Map{"error": "Ningún archivo recibido"})
|
|
}
|
|
|
|
// fecha_expedicion puede venir como un array (uno por archivo) o uno solo compartido
|
|
fechas := form.Value["fecha_expedicion"]
|
|
nombres := form.Value["nombre"]
|
|
|
|
uploadDir := fmt.Sprintf("uploads/clientes/%d", id)
|
|
if err := os.MkdirAll(uploadDir, 0750); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "No se pudo crear directorio"})
|
|
}
|
|
|
|
var creados []models.ClienteDocumento
|
|
for i, fh := range files {
|
|
// Validar tamaño máximo: 20 MB
|
|
if fh.Size > 20*1024*1024 {
|
|
return c.Status(400).JSON(fiber.Map{"error": fmt.Sprintf("Archivo '%s' supera 20 MB", fh.Filename)})
|
|
}
|
|
|
|
// Validar extensión permitida
|
|
if !extensionPermitida(fh.Filename) {
|
|
return c.Status(400).JSON(fiber.Map{"error": fmt.Sprintf("Tipo de archivo no permitido: %s", fh.Filename)})
|
|
}
|
|
|
|
safeName := sanitizeFilename(fh.Filename)
|
|
destPath := filepath.Join(uploadDir, fmt.Sprintf("%d_%s", time.Now().UnixNano(), safeName))
|
|
|
|
if err := saveUploadedFile(fh, destPath); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Error guardando archivo: " + err.Error()})
|
|
}
|
|
|
|
doc := models.ClienteDocumento{
|
|
ClienteID: uint(id),
|
|
Archivo: destPath,
|
|
OriginalName: fh.Filename,
|
|
TipoMime: fh.Header.Get("Content-Type"),
|
|
Tamanio: fh.Size,
|
|
}
|
|
|
|
// Nombre descriptivo
|
|
if i < len(nombres) && strings.TrimSpace(nombres[i]) != "" {
|
|
doc.Nombre = strings.TrimSpace(nombres[i])
|
|
} else {
|
|
doc.Nombre = fh.Filename
|
|
}
|
|
|
|
// Fecha de expedición
|
|
if i < len(fechas) && fechas[i] != "" {
|
|
if t, err := time.Parse("2006-01-02", fechas[i]); err == nil {
|
|
doc.FechaExpedicion = &t
|
|
}
|
|
} else if len(fechas) == 1 && fechas[0] != "" {
|
|
if t, err := time.Parse("2006-01-02", fechas[0]); err == nil {
|
|
doc.FechaExpedicion = &t
|
|
}
|
|
}
|
|
|
|
if err := models.CreateClienteDocumento(&doc); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": "Error guardando en BD: " + err.Error()})
|
|
}
|
|
creados = append(creados, doc)
|
|
}
|
|
|
|
return c.Status(201).JSON(fiber.Map{"ok": true, "creados": len(creados), "documentos": creados})
|
|
}
|
|
|
|
// DeleteClienteDocumento elimina un documento y su archivo en disco.
|
|
func DeleteClienteDocumento(c *fiber.Ctx) error {
|
|
docID, err := strconv.ParseUint(c.Params("docID"), 10, 32)
|
|
if err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
|
}
|
|
|
|
doc, err := models.GetClienteDocumentoByID(uint(docID))
|
|
if err != nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Documento no encontrado"})
|
|
}
|
|
|
|
// Eliminar archivo físico (no fatal si no existe)
|
|
_ = os.Remove(doc.Archivo)
|
|
|
|
if err := models.DeleteClienteDocumento(uint(docID)); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"ok": true, "message": "Documento eliminado"})
|
|
}
|
|
|
|
// DownloadClienteDocumento sirve el archivo para descarga directa.
|
|
func DownloadClienteDocumento(c *fiber.Ctx) error {
|
|
docID, err := strconv.ParseUint(c.Params("docID"), 10, 32)
|
|
if err != nil {
|
|
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
|
}
|
|
|
|
doc, err := models.GetClienteDocumentoByID(uint(docID))
|
|
if err != nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Documento no encontrado"})
|
|
}
|
|
|
|
// Asegurarse de que el path no escape del directorio uploads/
|
|
cleanPath := filepath.Clean(doc.Archivo)
|
|
if !strings.HasPrefix(cleanPath, "uploads/") {
|
|
return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
|
|
}
|
|
|
|
return c.Download(cleanPath, doc.OriginalName)
|
|
}
|
|
|
|
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
var extensionesPermitidas = map[string]bool{
|
|
".pdf": true, ".doc": true, ".docx": true,
|
|
".xls": true, ".xlsx": true, ".csv": true,
|
|
".png": true, ".jpg": true, ".jpeg": true,
|
|
".gif": true, ".webp": true, ".txt": true,
|
|
".zip": true, ".rar": true,
|
|
}
|
|
|
|
func extensionPermitida(filename string) bool {
|
|
ext := strings.ToLower(filepath.Ext(filename))
|
|
return extensionesPermitidas[ext]
|
|
}
|
|
|
|
func sanitizeFilename(name string) string {
|
|
base := filepath.Base(name)
|
|
// Eliminar caracteres peligrosos
|
|
safe := strings.Map(func(r rune) rune {
|
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
|
|
(r >= '0' && r <= '9') || r == '.' || r == '-' || r == '_' {
|
|
return r
|
|
}
|
|
return '_'
|
|
}, base)
|
|
return safe
|
|
}
|
|
|
|
func saveUploadedFile(fh *multipart.FileHeader, dest string) error {
|
|
src, err := fh.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
|
|
out, err := os.Create(dest) //nolint:gosec
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
buf := make([]byte, 32*1024)
|
|
for {
|
|
n, err := src.Read(buf)
|
|
if n > 0 {
|
|
if _, werr := out.Write(buf[:n]); werr != nil {
|
|
return werr
|
|
}
|
|
}
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|