Hertz 学习笔记(9)

830 阅读2分钟

今天来学一些和文件有关的操作。

关于如何上传,下载文件和搭建静态文件服务的示例

目前有下载,上传,静态文件,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/filehttp://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
	}
}

这段代码里写了一个去路径斜杠的实现,最后返回去斜杠的结果,其他的代码和之前一样。