tusd 是基于golang 开发的对于tus 断点续传协议的实现,既可以做为server 使用,也可以使用golang 包,开发自己的文件存储服务
Github tus是一种基于HTTP的可恢复文件上传的协议。意味着上传可以随时中断,并可以恢复没有再次重新上传之前的数据。
安装,把它作为一个独立的工具:
git clone https://github.com/tus/tusd.git
go build -o tusd cmd/tusd/main.go
tusd -upload-dir=./data 指定上传目录
前端上传测试,uppy库看起来不错:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Uppy</title>
<link href="https://transloadit.edgly.net/releases/uppy/v1.0.0/uppy.min.css" rel="stylesheet">
</head>
<body>
<div id="drag-drop-area"></div>
<script src="https://transloadit.edgly.net/releases/uppy/v1.0.0/uppy.min.js"></script>
<script>
var uppy = Uppy.Core()
.use(Uppy.Dashboard, {
inline: true,
target: '#drag-drop-area'
})
.use(Uppy.Tus, {endpoint: 'http://localhost/files/'})
uppy.on('complete', (result) => {
console.log('Upload complete! We’ve uploaded these files:', result.successful)
})
</script>
</body>
</html>
也有通用的一个在线地址可供测试:http://fpcloud.ricorean.net/plugin/tus-js-client-master/demo/
也有与示例相仿的一个golang代码
package main
import (
"fmt"
"net/http"
"github.com/tus/tusd/pkg/filestore"
tusd "github.com/tus/tusd/pkg/handler"
)
func main() {
// 创建一个新的文件存储库实例,它负责将上载的文件存储在指定目录中的磁盘上。
// 此路径必须已存在
// 如果您想将它们保存在不同的介质上,例如远程FTP服务器,您可以通过实现tusd来实现您自己的存储后端。数据存储接口。
store := filestore.FileStore{
Path: "./uploads",
}
// tusd的存储后端可能包括多个不同的部分,它们处理上传创建、锁定、终止等。
// composer 是一个将所有这些分开的作品连接在一起的地方。在这个例子中,我们只使用文件存储,但您可以插入多个。
composer := tusd.NewStoreComposer()
store.UseIn(composer)
// 通过提供一个配置,为tusd服务器创建一个新的HTTP处理程序。
handler, err := tusd.NewHandler(tusd.Config{
BasePath: "/files/",
StoreComposer: composer,
NotifyCompleteUploads: true,
})
if err != nil {
panic(fmt.Errorf("Unable to create handler: %s", err))
}
// 启动另一个处理程序,用于在上传完成时从处理程序接收事件。该事件将包含有关上传本身和相关HTTP请求的详细信息。
go func() {
for {
event := <-handler.CompleteUploads
fmt.Printf("Upload %s finished\n", event.Upload.ID)
}
}()
// 现在,我们需要自己启动HTTP服务器。最后,tusd将开始监听并接受在 http://localhost:8080/files 上的请求
http.Handle("/files/", http.StripPrefix("/files/", handler))
err = http.ListenAndServe(":80", nil)
if err != nil {
panic(fmt.Errorf("Unable to listen: %s", err))
}
}
handler 有几只勾子可用:
CompleteUploads chan HookEvent
TerminatedUploads chan HookEvent
UploadProgress chan HookEvent 上传进度用于发送关于当前正在运行的上传进度的通知。
CreatedUploads chan HookEvent
tusd -host 127.0.0.1 -port 1337 修改端口与IP
tusd -base-path /api/uploads 可以通过向上传创建端点发送一个POST请求来创建上传。
tusd -max-size 1000000000 最大上传1GB
tusd -disable-download 禁止下载
tusd -disable-termination 是否允许中断(中断后将删除)
tusd -upload-dir=./uploads 上传目录
添加进度勾子
package main
import (
"log"
"net/http"
"github.com/tus/tusd/v2/pkg/filelocker"
"github.com/tus/tusd/v2/pkg/filestore"
tusd "github.com/tus/tusd/v2/pkg/handler"
)
func main() {
store := filestore.New("./uploads")
locker := filelocker.New("./uploads")
composer := tusd.NewStoreComposer()
store.UseIn(composer)
locker.UseIn(composer)
handler, err := tusd.NewHandler(tusd.Config{
BasePath: "/files/",
StoreComposer: composer,
NotifyCompleteUploads: true,
NotifyUploadProgress: true, // 需要这个回调勾子
})
if err != nil {
log.Fatalf("unable to create handler: %s", err)
}
go func() {
for {
// event := <-handler.CompleteUploads
// log.Printf("Upload %s finished\n", event.Upload.ID)
event := <-handler.UploadProgress
log.Printf("进度%0.2f%%\n", float64(event.Upload.Offset)/float64(event.Upload.Size)*100)
}
}()
http.Handle("/files/", http.StripPrefix("/files/", handler))
http.Handle("/files", http.StripPrefix("/files", handler))
err = http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatalf("unable to listen: %s", err)
}
}
以下为简单翻译,未测试:
https://tus.github.io/tusd/advanced-topics/hooks/
文件钩子:执行提供的可执行文件或脚本
HTTP钩子:发送HTTP POST请求到一个自定义端点
gPRC钩子:调用远程gR端点上的方法
插件钩子:从磁盘加载一个插件并调用它的方法
默认情况下,文件钩子系统被禁用。
若要启用它,请将-钩子-dir选项传递给tusd二进制文件。
该标志的值将是一个路径,即钩子目录,相对于当前的工作目录,指向包含可执行的钩子文件的文件夹:
tusd -hooks-dir ./path/to/hooks/
将运行此目录下与事件相同文件名的程序(Linux下不能有扩展名,Win下可以bat和exe)
HTTP(S)钩子
tusd -hooks-http http://localhost:8081/write
可以在示例代码中看到各种钩子的使用。
使用在线网址测试时(http://fpcloud.ricorean.net/plugin/tus-js-client-master/demo/)在http钩子部份,python示例似乎还是有点问题:pre-create事件时,似乎在等前端给它一个文件名,而前端并没有传来名字,导致出错。
Golang代码改如下,实现了指定上传文件的文件名:
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
uuid "github.com/satori/go.uuid"
)
type HTTPHookHandler struct {
http.Handler
}
func (h *HTTPHookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello! This server only responds to POST requests"))
case "POST":
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var hookRequest map[string]interface{}
err = json.Unmarshal(body, &hookRequest)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
h.handlePostRequest(w, hookRequest)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
w.Write([]byte("Unsupported method"))
}
}
func (h *HTTPHookHandler) handlePostRequest(w http.ResponseWriter, hookRequest map[string]interface{}) {
fmt.Println("---------------------------------------------------------")
fmt.Println("Received hook request:")
fmt.Println(hookRequest)
fmt.Println("---------------------------------------------------------")
hookResponse := map[string]interface{}{
"HTTPResponse": map[string]interface{}{
"Headers": make(map[string]string),
},
}
if hookRequestType, ok := hookRequest["Type"].(string); ok {
if strings.EqualFold(hookRequestType, "pre-create") {
metaData, _ := hookRequest["Event"].(map[string]interface{})["Upload"].(map[string]interface{})["MetaData"].(map[string]interface{})
fmt.Println(metaData, "---------")
// isValid := metaData["filename"] != nil
// if !isValid {
// hookResponse["RejectUpload"] = true
// hookResponse["HTTPResponse"].(map[string]interface{})["StatusCode"] = 400
// hookResponse["HTTPResponse"].(map[string]interface{})["Body"] = "no filename provided"
// hookResponse["HTTPResponse"].(map[string]interface{})["Headers"].(map[string]string)["X-Some-Header"] = "yes"
// } else {
uuid := uuid.NewV4() // 使用NewV4()生成一个随机的版本4的UUID
hookResponse["ChangeFileInfo"] = map[string]interface{}{
"ID": fmt.Sprintf("prefix-%s", uuid.String()),
"MetaData": map[string]interface{}{
"filename": metaData["filename"],
"creation_time": time.Now().Format(time.RFC1123),
},
}
//}
} else if strings.EqualFold(hookRequestType, "post-finish") {
id, _ := hookRequest["Event"].(map[string]interface{})["Upload"].(map[string]interface{})["ID"].(string)
size, _ := hookRequest["Event"].(map[string]interface{})["Upload"].(map[string]interface{})["Size"].(float64)
storage, _ := hookRequest["Event"].(map[string]interface{})["Upload"].(map[string]interface{})["Storage"].(string)
fmt.Printf("Upload %s (%0.0f bytes) is finished. Find the file at:\n", id, size)
fmt.Println(storage)
}
}
fmt.Println("Responding with hook response:")
fmt.Println(hookResponse)
responseBody, err := json.Marshal(hookResponse)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(responseBody)
}
func main() {
server := &http.Server{
Addr: ":8000",
Handler: &HTTPHookHandler{},
}
log.Fatal(server.ListenAndServe())
}