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
“:
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.
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.