(转) tusd断点续传库

声明:内容源自网络,版权归原作者所有。若有侵权请在网页聊天中联系我

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())
}

相关文章