Guías
Carga de archivos
Buffalo permite un manejo más fácil de archivos cargados desde un formulario. Almacenar estos archivos, tanto en el disco o S3, depende de ti, el desarrollador final: Buffalo solo te brinda el fácil acceso al archivo desde la solicitud.
Configuración del formulario
El helper de formulario f.FileTag
se puede usar para agregar rápidamente un archivo al formulario. Al usar esto, el enctype
del formulario cambia automaticamente a multipart/form-data
.
<%= form_for(widget, {action: widgetsPath(), method: "POST"}) { %>
<%= f.InputTag("Name") %>
<%= f.FileTag("MyFile", {name: "someFile"}) %>
<button class="btn btn-success" role="submit">Save</button>
<a href="<%= widgetsPath() %>" class="btn btn-warning" data-confirm="Are you sure?">Cancel</a>
<% } %>
Accediendo al archivo del formulario
buffalo.Context
tiene un método c.File
que toma una cadena como parámetro, el name
del input de archivo del formulario, y devolverá un binding.File
que se puede usar para recuperar fácilmente un archivo desde este.
func SomeHandler(c buffalo.Context) error {
// ...
f, err := c.File("someFile")
if err != nil {
return errors.WithStack(err)
}
// ...
}
Mapeando a una estructura
El c.Bind
permite vincular elementos del formulario a una estructura, pero también puede adjuntar archivos cargados a la estructura. Para esto, el tipo del campo de la estructura DEBE ser de tipo binding.File
En el siguiente ejemplo podrás ver un modelo, el cual está configurado para tener un campo MyFile
de tipo binding.File
. Hay un callback AfterCreate
en este modelo de ejemplo que guarda el archivo en el disco después que el modelo se haya guardado correctamente en la base de datos.
// models/widget.go
type Widget struct {
ID uuid.UUID `json:"id" db:"id"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
Name string `json:"name" db:"name"`
MyFile binding.File `db:"-" form:"someFile"`
}
func (w *Widget) AfterCreate(tx *pop.Connection) error {
if !w.MyFile.Valid() {
return nil
}
dir := filepath.Join(".", "uploads")
if err := os.MkdirAll(dir, 0755); err != nil {
return errors.WithStack(err)
}
f, err := os.Create(filepath.Join(dir, w.MyFile.Filename))
if err != nil {
return errors.WithStack(err)
}
defer f.Close()
_, err = io.Copy(f, w.MyFile)
return err
}
MyFile
no se guarda en la base de datos debido al tag de estructura db:"-"
.
Probando la carga de archivos
La librería de prueba de HTTP github.com/gobuffalo/httptest
(incluida en el paquete github.com/gobuffalo/suite
que usa Buffalo para pruebas) ha sido actualizado para incluir dos nuevas funciones: MultiPartPost
y MultiPartPut
.
Estos métodos funcionan como los métodos Post
y Put
, pero en su lugar, envía formularios multipart
, y pueden aceptar archivos para cargar.
Al igual que Post
y Put
; MultiPartPost
and MultiPartPut
, toman una estructura o mapa como el primer argumento: Esto es el equivalente al formulario de HTML que se enviará. Los metodos toman un segundo armumento variable httptest.File
.
Un httptest.File
requiere el nombre del parámetro del formulario, ParanName
; el nombre del archivo, FileName
; y un io.Reader
, presumiblemente el archivo que deseas cargar.
// actions/widgets_test.go
func (as *ActionSuite) Test_WidgetsResource_Create() {
// clear out the uploads directory
os.RemoveAll("./uploads")
// setup a new Widget
w := &models.Widget{Name: "Foo"}
// find the file we want to upload
r, err := os.Open("./logo.svg")
as.NoError(err)
// setup a new httptest.File to hold the file information
f := httptest.File{
// ParamName is the name of the form parameter
ParamName: "someFile",
// FileName is the name of the file being uploaded
FileName: r.Name(),
// Reader is the file that is to be uploaded, any io.Reader works
Reader: r,
}
// Post the Widget and the File(s) to /widgets
res, err := as.HTML("/widgets").MultiPartPost(w, f)
as.NoError(err)
as.Equal(302, res.Code)
// assert the file exists on disk
_, err = os.Stat("./uploads/logo.svg")
as.NoError(err)
// assert the Widget was saved to the DB correctly
as.NoError(as.DB.First(w))
as.Equal("Foo", w.Name)
as.NotZero(w.ID)
}
// actions/widgets.go
// Create adds a Widget to the DB. This function is mapped to the
// path POST /widgets
func (v WidgetsResource) Create(c buffalo.Context) error {
// Allocate an empty Widget
widget := &models.Widget{}
// Bind widget to the html form elements
if err := c.Bind(widget); err != nil {
return errors.WithStack(err)
}
// Get the DB connection from the context
tx, ok := c.Value("tx").(*pop.Connection)
if !ok {
return errors.WithStack(errors.New("no transaction found"))
}
// Validate the data from the html form
verrs, err := tx.ValidateAndCreate(widget)
if err != nil {
return errors.WithStack(err)
}
if verrs.HasAny() {
// Make widget available inside the html template
c.Set("widget", widget)
// Make the errors available inside the html template
c.Set("errors", verrs)
// Render again the new.html template that the user can
// correct the input.
return c.Render(422, r.HTML("widgets/new.html"))
}
// If there are no errors set a success message
c.Flash().Add("success", "Widget was created successfully")
// and redirect to the widgets index page
return c.Redirect(302, "/widgets/%s", widget.ID)
}
// models/widgets.go
package models
import (
"encoding/json"
"io"
"os"
"path/filepath"
"time"
"github.com/gobuffalo/buffalo/binding"
"github.com/gobuffalo/pop/v6"
"github.com/gobuffalo/validate/v3"
"github.com/gobuffalo/validate/v3/validators"
"github.com/pkg/errors"
"github.com/gofrs/uuid"
)
type Widget struct {
ID uuid.UUID `json:"id" db:"id"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
Name string `json:"name" db:"name"`
MyFile binding.File `db:"-" form:"someFile"`
}
// String is not required by pop and may be deleted
func (w Widget) String() string {
jw, _ := json.Marshal(w)
return string(jw)
}
// Widgets is not required by pop and may be deleted
type Widgets []Widget
// String is not required by pop and may be deleted
func (w Widgets) String() string {
jw, _ := json.Marshal(w)
return string(jw)
}
func (w *Widget) AfterCreate(tx *pop.Connection) error {
if !w.MyFile.Valid() {
return nil
}
dir := filepath.Join(".", "uploads")
if err := os.MkdirAll(dir, 0755); err != nil {
return errors.WithStack(err)
}
f, err := os.Create(filepath.Join(dir, w.MyFile.Filename))
if err != nil {
return errors.WithStack(err)
}
defer f.Close()
_, err = io.Copy(f, w.MyFile)
return err
}
// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method.
// This method is not required and may be deleted.
func (w *Widget) Validate(tx *pop.Connection) (*validate.Errors, error) {
return validate.Validate(
&validators.StringIsPresent{Field: w.Name, Name: "Name"},
), nil
}
// ValidateCreate gets run every time you call "pop.ValidateAndCreate" method.
// This method is not required and may be deleted.
func (w *Widget) ValidateCreate(tx *pop.Connection) (*validate.Errors, error) {
return validate.NewErrors(), nil
}
// ValidateUpdate gets run every time you call "pop.ValidateAndUpdate" method.
// This method is not required and may be deleted.
func (w *Widget) ValidateUpdate(tx *pop.Connection) (*validate.Errors, error) {
return validate.NewErrors(), nil
}