文件上传
从 v0.10.3开始
Buffalo 允许轻松处理从表单上载的文件。 存储这些文件,比如到磁盘或 S3,取决于最终的开发人员: Buffalo 只是让您能够轻松地从请求访问文件。
配置表单
可以使用 f.FileTag
表单帮助器快速向表单添加file
元素。 当使用这个方法时,表单的enctype
类型会自动切换为multipart / form-data
。
接收表单文件
在buffalo.Context
c.File
接受一个字符串,表单文件参数的名称,并将返回一个binding.File
func SomeHandler(c buffalo.Context) error {
// ...
f, err := c.File("someFile")
if err != nil {
return errors.WithStack(err)
}
// ...
}
绑定到结构体
c.Bind
允许将表单元素绑定到结构,但它也可以将上传的文件附加到结构。 为此,struct 属性的类型必须是binding.File
类型。
在下面的示例中,您可以看到一个模型,它被配置为具有类型绑定的 MyFile 属性。 这个示例模型上有一个 AfterCreate
回调,在模型成功保存到数据库之后将文件保存到磁盘。
// 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
}
注意: 由于 db: “-”结构标记,MyFile 属性没有保存到数据库中。
测试文件上传
Http 测试库, github.com/gobuffalo/httptest 文件包(包含在布法罗测试用的 github.com/gobuffalo/suite 文件包中)已经更新,包含了两个新功能: MultiPartPost
和 MultiPartPut
。
这些方法的工作原理和 Post
和 Put
方法一样,但是它们提交的是一个多部分的表单,并且可以接受上传的文件。
与 Post
和 Put
、 MultiPartPost
和 MultiPartPut
一样,第一个参数采用结构或映射: 这相当于您发布的 HTML 表单。 这些方法采用一个可变参数的第二个参数 httptest.File
一个httptest.File
文件需要表单参数的名称 ParamName
、文件的名称 FileName
和 io.Reader
,大概是你想要上传的文件。
/ 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"
"github.com/markbates/validate"
"github.com/markbates/validate/validators"
"github.com/pkg/errors"
"github.com/satori/go.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
}
本作品采用《CC 协议》,转载必须注明作者和本文链接