今天来学一些和文件有关的操作。
关于如何上传,下载文件和搭建静态文件服务的示例
目前有下载,上传,静态文件,HTML,HTML 文件系统五个例子,下面分别来看。
下载
代码很简单,有两种方式:
/*
* Copyright 2022 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"context"
"net/url"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
)
func main() {
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
// FileAttachment() sets the "content-disposition" header and returns the file as an "attachment".
h.GET("/fileAttachment", func(ctx context.Context, c *app.RequestContext) {
// If you use Chinese, need to encode
fileName := url.QueryEscape("hertz")
c.FileAttachment("./file/download/file.txt", fileName)
})
// File() will return the contents of the file directly
h.GET("/file", func(ctx context.Context, c *app.RequestContext) {
c.File("./file/download/file.txt")
})
h.Spin()
}
然后在同一个文件夹里面放上一个 file.txt 文件,里面内容随意。运行这个代码:
go run file/download/main.go
然后用浏览器打开 http://127.0.0.1:8080/file 和 http://127.0.0.1:8080/fileAttachment。
里面的两个方法不太一样,File() 这个方法直接返回文件,FileAttachment() 这个方法先设置一个内容部署的标头,告诉浏览器这个 file.txt 应该是一个附件然后推荐了一个名称 hertz。
上传
服务端的代码如下:
/*
* Copyright 2022 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"context"
"fmt"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
func main() {
// WithMaxRequestBodySize can set the size of the body
h := server.Default(server.WithHostPorts("127.0.0.1:8080"), server.WithMaxRequestBodySize(20<<20))
h.POST("/singleFile", func(ctx context.Context, c *app.RequestContext) {
// single file
file, _ := c.FormFile("file")
fmt.Println(file.Filename)
// Upload the file to specific dst
c.SaveUploadedFile(file, fmt.Sprintf("./file/upload/%s", file.Filename))
c.String(consts.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
})
h.POST("/multiFile", func(ctx context.Context, c *app.RequestContext) {
// Multipart form
form, _ := c.MultipartForm()
files := form.File["file"]
for _, file := range files {
fmt.Println(file.Filename)
// Upload the file to specific dst.
c.SaveUploadedFile(file, fmt.Sprintf("./file/upload/%s", file.Filename))
}
c.String(consts.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
})
h.Spin()
}
上传的代码同样是需要配合客户端的代码来使用,先跑这个服务端的代码:
go run file/upload/main.go
然后跑客户端的代码,这一头才能上传文件:
go run client/upload_file/main.go
客户端的代码我先跳过了,之后还会遇到的。服务端的代码里有两个 POST 请求类型,分别对应单文件和多文件上传。多文件实际上还是用表单,然后写个循环一个文件一个文件存。
静态文件
首先介绍目录结构,这个例子里面有三个文件夹和五个 txt 文件,我用 tree 命令的形式把这些信息打出来:
$ tree .
.
├── static
│ └── 1.txt: hello hertz
├── txt
│ ├── 1.txt: hello hertz!!!!
│ └── 2.txt: hello hertz!
├── txts
│ ├── 1.txt: hertz
│ └── 2.txt: hertz!!!
├── README.md
└── main.go
接下来进入代码:
/*
* Copyright 2022 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"context"
"time"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
func main() {
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
h.Static("/static", "./")
h.StaticFile("/main", "./main.go")
indexNames := []string{"1.txt", "2.txt"}
h.StaticFS("/static1", &app.FS{
Root: "./",
PathRewrite: app.NewPathSlashesStripper(1),
PathNotFound: func(_ context.Context, ctx *app.RequestContext) {
ctx.JSON(consts.StatusNotFound, "The requested resource does not exist")
},
CacheDuration: time.Second * 5,
IndexNames: indexNames,
Compress: true,
CompressedFileSuffix: "hertz",
AcceptByteRange: true,
})
h.StaticFS("/static2", &app.FS{
PathRewrite: app.NewPathSlashesStripper(1),
GenerateIndexPages: true,
})
h.Spin()
}
要运行这个例子的代码,只需要在这个文件夹里面跑:
go run main.go
然后静态文件系统就起来了。
注意到这个代码里用到了结构体 FS,这个结构体的定义是:
type FS struct {
noCopy nocopy.NoCopy
Root string
IndexNames []string
GenerateIndexPages bool
Compress bool
AcceptByteRange bool
PathRewrite PathRewriteFunc
PathNotFound HandlerFunc
CacheDuration time.Duration
CompressedFileSuffix string
once sync.Once
h HandlerFunc
}
例子里面一共有两次创建 FS 对象,这里只介绍这两次用到的属性:
Root:保存文件的根目录。用浏览器打开 http://127.0.0.1:8080/static/1.txt 就能得到./static/1.txt。IndexNames:设置indexNames,当访问这个目录的时候,可以得到这个切片代表的全部文件里的其中之一。还是用浏览器,打开 http://127.0.0.1:8080/static1/txt/,每次的结果是随机的。GenerateIndexPages:设置这个为 true 之后,访问目录就能得到文件的索引而不是名称。还是打开浏览器,访问 http://127.0.0.1:8080/static2/txts。Compress:设置为 true 之后服务器会把CompressedFileSuffix当后缀添加到原来的文件名后面。CompressedFileSuffix:一段后缀,如果服务器尝试压缩文件,这个就是压缩结果的文件名里面的后缀。AcceptByteRange:设置为 true 以后客户端就可以向服务端请求某个文件指定的一段字节。PathRewrite:用NewPathSlashesStripper()来设置这个字段,然后会返回一个 path rewriter 把文件路径里的斜杠去掉。还是用浏览器,打开 http://127.0.0.1:8080/static1/txt/2.txt。PathNotFound:设置一个处理逻辑,文件没找到就进这个逻辑。同样打开浏览器,访问 http://127.0.0.1:8080/static1/hertz。CacheDuration:设置缓存的到期时间,时间到了就自动关掉不活跃的文件 handlers。
也许我的理解有错,前面我给出了当前我自己查到的源码里的结构体定义位置,欢迎挑错。
HTML
终于到了一个简单点的了。。。还是先从例子的目录结构开始:
$ tree .
.
├── assets/css
│ └── style.css
├── views
│ └── index.html
├── README.md
└── main.go
运行的效果你们也能想象出来,访问本地的一个端口然后能得到一个网页(静态),然后 CSS 生效。
代码如下:
/*
* Copyright 2022 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
)
func main() {
h := server.Default(server.WithHostPorts("127.0.0.1:8081"))
h.LoadHTMLGlob("views/*")
h.Static("/", "./assets")
h.GET("/", func(c context.Context, ctx *app.RequestContext) {
ctx.HTML(200, "index.html", nil)
})
h.Spin()
}
HTML 文件系统
这个例子的目录结构和前面 HTML 例子一样,然后不同的地方就在源码里:
/*
* Copyright 2022 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"context"
"strings"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
)
func main() {
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
h.LoadHTMLGlob("views/*")
prefix := "/public"
root := "./assets"
fs := &app.FS{Root: root, PathRewrite: getPathRewriter(prefix)}
h.StaticFS(prefix, fs)
h.GET("/", func(c context.Context, ctx *app.RequestContext) {
ctx.HTML(200, "index.html", nil)
})
h.Spin()
}
func getPathRewriter(prefix string) app.PathRewriteFunc {
// Cannot have an empty prefix
if prefix == "" {
prefix = "/"
}
// Prefix always start with a '/' or '*'
if prefix[0] != '/' {
prefix = "/" + prefix
}
// Is prefix a direct wildcard?
isStar := prefix == "/*"
// Is prefix a partial wildcard?
if strings.Contains(prefix, "*") {
isStar = true
prefix = strings.Split(prefix, "*")[0]
// Fix this later
}
prefixLen := len(prefix)
if prefixLen > 1 && prefix[prefixLen-1:] == "/" {
// /john/ -> /john
prefixLen--
prefix = prefix[:prefixLen]
}
return func(ctx *app.RequestContext) []byte {
path := ctx.Path()
if len(path) >= prefixLen {
if isStar && string(path[0:prefixLen]) == prefix {
path = append(path[0:0], '/')
} else {
path = path[prefixLen:]
if len(path) == 0 || path[len(path)-1] != '/' {
path = append(path, '/')
}
}
}
if len(path) > 0 && path[0] != '/' {
path = append([]byte("/"), path...)
}
return path
}
}
这段代码里写了一个去路径斜杠的实现,最后返回去斜杠的结果,其他的代码和之前一样。