12. Archivos embebidos

La biblioteca estandar de Go incluse un paquete embed que permite insetar archivos externos a tu programa.

En este post vamos a actualizar nuestra app para que incluya los archivos de nuestro directorio ui comenzando con los archivos estaticos css, js y las imagenes, continuando luego con las plantillas HTML

Usar embed es totalmente opcional, pero nos da la posibilidad de que nuestros programas sean self-contained y tenga todo lo que necesita para correr como parte del binario compilado. A su vez facilita el deploy o la distribucion de la aplicacion web.

12.1 Incluyendo archivos estaticos

Creemos ui/efs.go

$ touch ui/efs.go
package ui

import (
	"embed"
)

//go:embed "static"
var Files embed.FS

La linea importante es //go:embed “static”

Se ve como un comentario pero es una directiva de comentario especial. Cuando nuestra app es compilada con go build o go run, este comentario le dice a Go que almacene los archivos de ui/static en un embedded filesystem referenciado a la variable global Files.

Hay que explicar algunas cosas.

  • La directiva comentario tiene que estar inmediatamente por encima de la variable la cual queres que almacene los archivos embebidos.
  • La directiva tiene el formato go:embed "<path>". El path es relativo al archivo .go que contiene la directiva, en nuestro caso go:embed “static” incluye el directorio ui/static de nuetro proyecto.
  • Solo podes usar la directiva go:embed en variables globales a nivel del paquete, no dentro de funciones o metodos. Si intentas hacerlo dentro de una funcion/metodo arrojara el error: “go:embed cannot apply to var inside func” en tiempo de compilacion.
  • Los paths no pueden contener . o .. ni podran comenzar o terminar con /. Esto es asi para asegurar que solo incluyas archivos o directorios que estan dentro del mismo directorio que el archivo .go que contiene la directiva go:embed. *Cuando utilizas la directiva go:embed para incrustar archivos en tu código, el sistema de archivos incrustado siempre tiene su raíz en el directorio que contiene la directiva go:embed. En el ejemplo mencionado, se hace referencia a una variable llamada Files, que contiene un sistema de archivos incrustado (embed.FS), y la raíz de ese sistema de archivos es el directorio “ui”.

Entonces, en este caso específico, si tienes archivos incrustados utilizando go:embed, esos archivos se considerarán como si estuvieran en el directorio “ui” de tu proyecto. Esto significa que puedes acceder a esos archivos incrustados utilizando rutas relativas a la carpeta “ui” en tu código, como si estuvieras navegando en el sistema de archivos normal.

En resumen, el paquete “embed” en Go arraiga el sistema de archivos incrustado en el directorio que contiene la directiva go:embed, lo que facilita el acceso a los archivos incrustados utilizando rutas relativas desde ese directorio.

12.1 Usando los archivos estaticos embebidos

Ahora cambiemos algunas cosas para que sirva los archivos estaticos desde el sistema de archivos embebidos, en vez de leerlo desde el disco en tiempo de ejecucion.

Vamos a cmd/web/routes.go

  package main
  
  import (
  	"net/http"
+    "snippetbox.alexedwards.net/ui"
  	"github.com/julienschmidt/httprouter"
  	"github.com/justinas/alice" 
  )
  
  func (app *application) routes() http.Handler {
  
  	router := httprouter.New()
  
  	router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		app.notFound(w)
  	})
  
-  	fileServer := http.FileServer(http.Dir("./ui/static/"))
-  	router.Handler(http.MethodGet, "/static/*filepath", http.StripPrefix("/static", fileServer))
    	// Take the ui.Files embedded filesystem and convert it to a http.FS type so
+	// that it satisfies the http.FileSystem interface. We then pass that to the
+	// http.FileServer() function to create the file server HTTP handler.
+	fileServer := http.FileServer(http.FS(ui.Files))
+	// Our static files are contained in the "static" folder of the ui.Files
+	// embedded filesystem. So, for example, our CSS stylesheet is located at
+	// "static/css/main.css". This means that we no longer need to strip the
+	// prefix from the request URL -- any requests that start with /static/ can
+	// just be passed directly to the file server and the corresponding static
+	// file will be served (so long as it exists).
+	router.Handler(http.MethodGet, "/static/*filepath", fileServer)
  	
  	dynamic := alice.New(app.sessionManager.LoadAndSave, noSurf, app.authenticate)
  
  	router.Handler(http.MethodGet, "/", dynamic.ThenFunc(app.home))
  	router.Handler(http.MethodGet, "/snippet/view/:id", dynamic.ThenFunc(app.snippetView))
  	router.Handler(http.MethodGet, "/user/signup", dynamic.ThenFunc(app.userSignup))
  	router.Handler(http.MethodPost, "/user/signup", dynamic.ThenFunc(app.userSignupPost))
  	router.Handler(http.MethodGet, "/user/login", dynamic.ThenFunc(app.userLogin))
  	router.Handler(http.MethodPost, "/user/login", dynamic.ThenFunc(app.userLoginPost))
  
  	protected := dynamic.Append(app.requireAuthentication)
  	router.Handler(http.MethodGet, "/snippet/create", protected.ThenFunc(app.snippetCreate))
  	router.Handler(http.MethodPost, "/snippet/create", protected.ThenFunc(app.snippetCreatePost))
  	router.Handler(http.MethodPost, "/user/logout", protected.ThenFunc(app.userLogoutPost))
  
  	standard := alice.New(app.recoverPanic, app.logRequest, sercureHeaders)
  
  	return standard.Then(router)
  }

Si guardas y renicias el server, todo deberia sergur funcionando.

12.1 Informacion adicional

12.1 Multiples paths

Podemos embeber multiples paths con una sola directiva asi:

//go:embed "static/css" "static/img" "static/js"
var Files embed.FS

Los separadores en los paths siempre tienen que ser / incluso en sistemas windows.

12.1 Embeber un archivo especifico

Si por ejemplo tenemos archivos que no queremos incluir como sass o less podemos solo incluir el main.css.

//go:embed "static/css/main.css" "static/img" "static/js"
var Files embed.FS
12.1 Comodin en los paths

Podemos usar * como comodin asi:

//go:embed "static/css/*.css" "static/img" "static/js"
var Files embed.FS
12.1 El prefijo all

Si una ruta es un directorio enteonces los archivos se agregaran de maanera recursiva , excepto quienes tengan un nombre que empiece con _ o ., Si queres incluirlos tambien podes usar el prefijo all.

//go:embed "all:static"
var Files embed.FS

12.2 Embebiendo templates HTML

Vamos a actualizar nuetra aplicacion para que el cache del template use el HTML embebido en vez de que los lea desde el disco en tiempo de ejecucion.

Volvamos a ui/efs.go y actualicemoslo para que ui.Files embeba el contenido del directorio ui/html que tambien contiene nuestros templates.

  package ui
  
  import (
  	"embed"
  )
  
+  //go:embed "html" "static"
  var Files embed.FS
  

Luego tenemos que actualizar la funcion newTemplateCache() en cmd/web/templates.go . Para que lea el template desde ui.Files. Para hacer esto , tenemos que aprovechar un par de caracteristicas especiales que go tiene para trabajar con filesystems embebidos:

  • La funcion fs.Glob() retorna un slice de filepaths que coinciden con un patron global.Es efectivamente lo mismo que la funcion filepath.Glob que usamos en post anteriores, excepto que este funciona con filesystems embebidos.

  • El metodo Template.ParseFS() puede ser usado para parsear templates HTML desde un filesystem embebido en un template set.Esto remplaza a los dos metodos Template.ParseFiles() y Template.ParseGlob() que usamos antes.Template.ParseFiles() es una variaddic function que permite parsear multiples templates en una sola llamada a ParseFiles().

  func newTemplateCache() (map[string]*template.Template, error) {
  	// Initialize a new map to act as the cache.
  	cache := map[string]*template.Template{}
  
+  // Use fs.Glob() to get a slice of all filepaths in the ui.Files embedded
+	// filesystem which match the pattern 'html/pages/*.html'. This essentially
+	// gives us a slice of all the 'page' templates for the application, just
+	// like before.
+	pages, err := fs.Glob(ui.Files, "html/pages/*.html") 

      if err != nil {
  		  return nil, err
  	  }
  
  	for _, page := range pages {
  
  		name := filepath.Base(page)
  
-  		ts, err := template.New(name).Funcs(functions).ParseFiles("./ui/html/base.html")
-  		if err != nil {
-  			return nil, err
-  		}
-  
-  		ts, err = ts.ParseGlob("./ui/html/partials/*.html")
-  		if err != nil {
-  			return nil, err
-  		}
-  
-  		ts, err = ts.ParseFiles(page)
+      // Create a slice containing the filepath patterns for the templates we
+		// want to parse.
+		patterns := []string{
+			"html/base.html",
+			"html/partials/*.html",
+			page,
+		}
+
+		// Use ParseFS() instead of ParseFiles() to parse the template files
+		// from the ui.Files embedded filesystem.
+		ts, err := template.New(name).Funcs(functions).ParseFS(ui.Files, patterns...)
  		if err != nil {
  			return nil, err
  		}
  
  		cache[name] = ts
  	}
  
  	return cache, nil
  }
  

Eso es todo cuando la app se compile en binario todos los archivos de UI estaran incluidos.

Puedes probar esto rápidamente creando un archivo binario ejecutable en tu directorio /tmp, copiando los certificados TLS y ejecutando el archivo binario. Así:

$ go build -o /tmp/web ./cmd/web/
$ cp -r ./tls /tmp/
$ cd /tmp/
$ ./web
time=2023-08-11T09:18:24.629+02:00 level=INFO msg="starting server" addr=:4000

Y vas a https://localhost:4000 deberia funcionar todo correctamente a pesar de que el binario no esta en un lugar donde puede tener acceso al disco.