Enviar archivo de JavaScript a Golang

Subir archivo de JavaScript a Go (Golang)

En este post de programación cliente-servidor vamos a ver cómo enviar un archivo desde JavaScript del lado del cliente a Golang (Go) del lado del servidor.

Específicamente hablando veremos cómo enviar un archivo usando fetch y FormData a través de AJAX hacia un servidor de Go.

Lo que te enseñaré servirá para enviar fotos, archivos binarios o incluso una foto tomada con la cámara web con las debidos ajustes.

Va a ser un ejemplo realmente simple pero que luego podrás modificar para, por ejemplo, usarlo en React, Angular, JavaScript puro o Vue.

Lado del cliente

Para este ejemplo te enseñaré a enviar el archivo y además un valor adicional de tipo cadena, ya que en otros tutoriales los lectores siempre me preguntan cómo enviar datos adicionales además del archivo.

El código queda así:

const archivos = $inputArchivo.files;
if (archivos.length <= 0) {
    return alert("No hay archivos seleccionados");
}
const primerArchivo = archivos[0];
const formdata = new FormData();
formdata.append("archivo", primerArchivo);
const nombre = "Parzibyte";// Dato de tipo cadena para ejemplificar
formdata.append("nombre", nombre);
const URL_SERVIDOR = "http://localhost:8080/foto"; // Servidor de Go
try {
    const response = await fetch(URL_SERVIDOR, {
        method: "POST",
        body: formdata,
    });
    const respuesta = await response.text();
    alert("El servidor dijo: " + respuesta)
} catch (e) {
    alert("Error en el servidor: " + e.message);
}

En la línea 1 obtenemos los archivos seleccionados en el input, aunque obviamente para este ejemplo solo enviamos uno.

Creamos el FormData en la línea 6 y además de agregarle el archivo (en la clave archivo) le agregamos un dato de tipo cadena (en la clave nombre).

Hacemos la petición usando fetch en la línea 12 y esperamos la respuesta. Si algo sale mal (que por ejemplo el servidor de Go rechace la petición porque la misma es muy grande) entonces se maneja en el catch.

Recuerda que debes modificar la URL del servidor dependiendo del puerto y ruta donde tu servidor de Golang esté escuchando.

Limitando tamaño de archivos

Pasemos a programar en Go. Aquí puedes usar cualquier framework o router, por ejemplo gorilla/mux, ya que al final accederemos a la petición de tipo *http.Request.

Cuando recibimos archivos en Golang debemos poner un límite de tamaño, ya que si alguien envía algo muy grande puede hacer que el servidor se “cuelgue”. Comenzamos limitando el tamaño de la petición:

// Prevenir que envíen peticiones muy grandes. Recuerda dejar espacio para máximo tamaño de foto + datos adicionales
r.Body = http.MaxBytesReader(w, r.Body, MaximoTamanioFotosEnBytes)

Y luego la leemos igualmente indicando el límite.

No te confundas, lo que te mostré anteriormente fue para rechazar la petición en caso de que supere el límite, y lo que viene a continuación es para cargar en la RAM solo los bytes permitidos y el sobrante colocarlo en el disco duro.

// Parsea y aloja en RAM o disco duro, dependiendo del límite que le indiquemos
err := r.ParseMultipartForm(MaximoTamanioFotosEnBytes)
if err != nil {
  log.Printf("Error al parsear: %v", err)
  return
}

Esto es importante, ya que por ejemplo si tú quisieras poner un límite de 1GB puedes hacerlo, pero al momento de invocar a ParseMultipartForm podrías pasar un límite de, por ejemplo, 5MB.

De ese modo podrías limitar peticiones a 1GB pero al parsearlas solo cargar 5MB en la RAM y el resto en el disco duro, ahorrando RAM. Yo uso el mismo valor en el ejemplo, pero no lo olvides.

Leyendo archivo y formulario recibido desde JavaScript

Ahora vamos a acceder al archivo que enviamos con fetch desde JavaScript, usando Go.

En este caso accedemos a r.MultipartForm.File pasándole la clave que queremos, esa misma clave que usamos al usar FormData en el cliente (archivo).

Por otro lado, para acceder a los valores que no son archivos (recuerda que enviamos una variable llamada nombre) podemos usar r.Form.Get pasando la clave que usamos en FormData igualmente.

encabezadosDeArchivos := r.MultipartForm.File["archivo"]

nombre := r.Form.Get("nombre")
log.Printf("Nombre: %v", nombre)

encabezadoPrimerArchivo := encabezadosDeArchivos[0]
primerArchivo, err := encabezadoPrimerArchivo.Open()
if err != nil {
  log.Printf("Error al abrir archivo: %v", err)
  return
}
defer primerArchivo.Close()

Y ya tenemos abierto el archivo, ahora debemos leerlo y guardarlo en otro lugar, cambiando además su nombre por seguridad.

Guardando archivo recibido en Go

Ya tenemos el archivo que enviaron desde JS, ya sea en la RAM o en el almacenamiento temporal. Ahora hay que copiarlo.

Yo recomiendo renombrarlo a algo aleatorio, conservando su extensión. Recuerda que para acceder al nombre con el que el archivo fue subido podemos acceder a encabezadoPrimerArchivo.Filename.

nombreArchivo := renombrarNombreDeArchivoAIdAleatorio(encabezadoPrimerArchivo.Filename)
archivoParaGuardar, err := os.Create(filepath.Join(DirectorioArchivos, nombreArchivo))
if err != nil {
  return
}
defer archivoParaGuardar.Close()
_, err = io.Copy(archivoParaGuardar, primerArchivo)
if err != nil {
  return
}
io.WriteString(w, "Subido correctamente")

En la línea 2 creamos el archivo en donde vamos a escribir al que recibimos en el formulario, luego copiamos en la línea 7 y finalmente en la línea 11 escribimos “Subido correctamente” en la respuesta.

Una nota sobre CORS

Si tú no sirves el HTML en el mismo host que Go, debes habilitar CORS y permitir el host desde donde haces las peticiones del cliente.

En el ejemplo ya he incluido las 2 cosas: cómo servir el HTML desde Go y cómo habilitar CORS. Puedes modificar todo de acuerdo a lo que necesites.

Poniendo todo junto

Entonces mi servidor de Go para recibir archivos y guardarlos queda como se ve a continuación, ya con todas las funciones:

package main

import (
	"fmt"      // Imprimir en consola
	"io"       // Ayuda a escribir en la respuesta
	"log"      //Loguear si algo sale mal
	"net/http" // El paquete HTTP
	"os"
	"path/filepath"

	"github.com/rs/xid"
)

func crearDirectorioSiNoExiste(directorio string) error {
	if _, err := os.Stat(directorio); os.IsNotExist(err) {
		err = os.Mkdir(directorio, 0755)
		if err != nil {
			return err
		}
		return nil
	} else {
		return err
	}
}

func obtenerIdAleatorioNoSeguro() string {
	guid := xid.New()
	return guid.String()
}

func renombrarNombreDeArchivoAIdAleatorio(nombreOriginal string) string {
	extension := filepath.Ext(nombreOriginal)
	return obtenerIdAleatorioNoSeguro() + extension
}

func main() {

	http.HandleFunc("/foto", func(w http.ResponseWriter, r *http.Request) {

		if r.Method != http.MethodPost {
			io.WriteString(w, "Solo se permiten peticiones POST")
			return
		}

		const MaximoTamanioFotosEnBytes = 5 << 20 // 5 megabytes, recuerda que debe haber espacio para tamaño foto + datos adicionales (o sea, formulario)
		const DirectorioArchivos = "subidas"
		crearDirectorioSiNoExiste(DirectorioArchivos)
		// CORS
		const HostPermitidoParaCORS = "http://localhost"

		w.Header().Set("Access-Control-Allow-Origin", HostPermitidoParaCORS)
		w.Header().Set("Access-Control-Allow-Credentials", "true")
		w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
		w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")

		// Prevenir que envíen peticiones muy grandes. Recuerda dejar espacio para máximo tamaño de foto + datos adicionales
		r.Body = http.MaxBytesReader(w, r.Body, MaximoTamanioFotosEnBytes)
		// Parsea y aloja en RAM o disco duro, dependiendo del límite que le indiquemos
		err := r.ParseMultipartForm(MaximoTamanioFotosEnBytes)
		if err != nil {
			log.Printf("Error al parsear: %v", err)
			return
		}
		encabezadosDeArchivos := r.MultipartForm.File["archivo"]

		nombre := r.Form.Get("nombre")
		log.Printf("Nombre: %v", nombre)

		encabezadoPrimerArchivo := encabezadosDeArchivos[0]
		primerArchivo, err := encabezadoPrimerArchivo.Open()
		if err != nil {
			log.Printf("Error al abrir archivo: %v", err)
			return
		}
		defer primerArchivo.Close()
		nombreArchivo := renombrarNombreDeArchivoAIdAleatorio(encabezadoPrimerArchivo.Filename)
		archivoParaGuardar, err := os.Create(filepath.Join(DirectorioArchivos, nombreArchivo))
		if err != nil {
			return
		}
		defer archivoParaGuardar.Close()
		_, err = io.Copy(archivoParaGuardar, primerArchivo)
		if err != nil {
			return
		}
		io.WriteString(w, "Subido correctamente")
	})

	http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("./public"))))
	direccion := ":8080" // Como cadena, no como entero; porque representa una dirección
	fmt.Println("Servidor listo escuchando en " + direccion)
	log.Fatal(http.ListenAndServe(direccion, nil))
}

Y junto a mi servidor tengo una carpeta llamada public en donde está el archivo enviar.html cuyo contenido es el siguiente:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Enviar archivo de JavaScript a Go</title>
</head>

<body>
    <label for="archivo">Selecciona un archivo:</label>
    <br>
    <input type="file" id="archivo">
    <br>
    <br>
    <button id="btnEnviar">Enviar</button>
</body>
<script>
    document.addEventListener("DOMContentLoaded", () => {
        const $inputArchivo = document.querySelector("#archivo"),
            $btnEnviar = document.querySelector("#btnEnviar");

        $btnEnviar.onclick = async () => {
            const archivos = $inputArchivo.files;
            if (archivos.length <= 0) {
                return alert("No hay archivos seleccionados");
            }
            const primerArchivo = archivos[0];
            const formdata = new FormData();
            formdata.append("archivo", primerArchivo);
            const nombre = "Parzibyte";// Dato de tipo cadena para ejemplificar
            formdata.append("nombre", nombre);
            const URL_SERVIDOR = "http://localhost:8080/foto"; // Servidor de Go
            try {
                const response = await fetch(URL_SERVIDOR, {
                    method: "POST",
                    body: formdata,
                });
                const respuesta = await response.text();
                alert("El servidor dijo: " + respuesta)
            } catch (e) {
                alert("Error en el servidor: " + e.message);
            }
        };

    });
</script>

</html>

Luego en la carpeta hacemos un go mod tidy o instalamos las dependencias manualmente y finalmente compilamos con go build, lo que nos dará un ejecutable llamado subir-archivo-go.

Al abrir el archivo nos indica que está escuchando en el puerto 8080, entonces visitamos http://localhost:8080/public/enviar.html, subimos un archivo y vemos que se ha alojado correctamente en el directorio “subidas“:

Enviar archivo de JavaScript a Golang
Enviar archivo de JavaScript a Golang

Por otro lado, si queremos enviar un archivo más grande que el límite nos aparecerá lo siguiente en el log del servidor y en el cliente.

Límite de peso de archivos al subir de JS a Go
Límite de peso de archivos al subir de JS a Go

Ahora que veo el log recuerdo que estábamos enviando una cadena además del archivo. En mi caso solo lo estoy imprimiendo para ejemplificar que sí estamos recibiendo correctamente el valor en el servidor.

Conclusión

El código fuente completo lo dejo en GitHub para que puedas explorarlo.

Cuando empecé a programar en el lado del servidor siempre usé PHP (y lo sigo usando) y al cambiar a Go me di cuenta de que tienes que hacer más cosas y no todo es automático, por ejemplo, en PHP usamos move_uploaded_file, configuramos el ini y todo queda perfecto, mientras que en Go debes personalizar todo escribiendo el código.

Incluso así, me gusta más Go, siento que tengo más control.

Para terminar te dejo con más tutoriales de Go en mi blog.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *