上一个教程解释了 tus 协议如何工作。如果你是 tus 的新手,我强烈建议你阅读 上一个教程。在本教程中,我们将创建数据模型和数据库 CRUD 方法。

本教程包含以下部分

  • 数据模型

  • 表创建

  • Tus 回顾

  • 生成文件

  • 更新文件

  • 获取文件

数据模型

我们先来讨论一下 tus 服务器的数据模型。 我们将使用 PostgreSQL 作为数据库。

我们的 tus 服务器需要一个表 file 来存储与文件相关的信息。 我们来讨论该表中应该包含哪些字段。

我们需要一个字段来唯一标识文件。 为了简单起见,我们将使用自动递增的 integer 字段 field_id 作为文件标识符。 该字段将是表的主键。 我们还将使用此 id 作为文件名。

接下来,我们的服务器需要跟踪每个文件的偏移量。 我们将使用 integer 字段field_offset 来存储文件偏移量。 我们将使用另一个 integer 字段file_upload_length 来存储文件的上传长度。

布尔字段 file_upload_complete 用于确定是否已上载整个文件。

我们还将拥有通常的审计字段 created_atmodified_at

这是表

  1. file_id SERIAL PRIMARY KEY
  2. file_offset INT NOT NULL
  3. file_upload_length INT NOT NULL
  4. file_upload_complete BOOLEAN NOT NULL
  5. created_at TIMESTAMP default NOW() not null
  6. modified_at TIMESTAMP default NOW() not null

表创建

我们将首先创建一个名为 fileserver 的数据库,然后编写代码来创建 file 表。

请使用以下命令切换到终端中的 psql 提示符

  1. $ \psql -U postgres

系统将提示您输入密码。成功登录后,您可以查看 postgres 命令提示符。

  1. postgres=# create database fileserver;

上面的命令将创建数据库 fileserver

现在我们已准备好数据库了,让我们继续并在代码中创建表。

  1. type fileHandler struct {
  2. db *sql.DB
  3. }
  4. func (fh fileHandler) createTable() error {
  5. q := `CREATE TABLE IF NOT EXISTS file(file_id SERIAL PRIMARY KEY,
  6. file_offset INT NOT NULL, file_upload_length INT NOT NULL, file_upload_complete BOOLEAN NOT NULL,
  7. created_at TIMESTAMP default NOW() NOT NULL, modified_at TIMESTAMP default NOW() NOT NULL)`
  8. _, err := fh.db.Exec(q)
  9. if err != nil {
  10. return err
  11. }
  12. log.Println("table create successfully")
  13. return nil
  14. }

我们有一个 fileHandler 结构,它包含一个字段 db ,它是数据库的句柄。这将在稍后从主要注入。在第5行我们添加了 createTable() 方法。如果表不存在,则此方法创建表,如果有则返回错误。

Tus协议回顾

在我们创建 DB CRUD 方法之前,让我们重新选择 tus 协议使用的 http 方法

POST - 创建新文件

PATCH - 将数据上传 Upload-Offset 到现有文件

HEAD - 获取到当前上传文件的 Upload-Offset 下载偏移量,以开始下个 patch 请求

我们将需要创建、更新和读取表操作来支持上述 http 方法。我们将在本教程中创建它们。

创建文件

在添加创建文件的方法之前,让我们先定义文件数据结构。

  1. type file struct {
  2. fileID int
  3. offset *int
  4. uploadLength int
  5. uploadComplete *bool
  6. }

上面的 file 结构表示一个文件。字段是一目了然的。我们为 offsetuploadLength 选择指针类型是有原因的,稍后将对此进行解释。

接下来,我们将添加将新行插入 file 表的方法。

  1. func (fh fileHandler) createFile(f file) (string, error) {
  2. cfstmt := `INSERT INTO file(file_offset, file_upload_length, file_upload_complete) VALUES($1, $2, $3) RETURNING file_id`
  3. fileID := 0
  4. err := fh.db.QueryRow(cfstmt, f.offset, f.uploadLength, f.uploadComplete).Scan(&fileID)
  5. if err != nil {
  6. return "", err
  7. }
  8. fid := strconv.Itoa(fileID)
  9. return fid, nil
  10. }

上面的方法将一行插入到 file 表中,并将 fileID 转换为string并返回它。这是很简单的。我们将 fileID 转换为 string 的原因是,稍后还将使用fileID作为文件的名称。

更新文件

我们现在编写文件更新方法。 在典型的文件中,我们只需要更新文件的 offsetuploadComplete 字段。 创建文件后,fileIDuploadLength 不会更改。 这也是我们在 file 结构中为 offsetuploadComplete 选择指针的原因。 如果 offsetuploadCompletenil,则表示未设置这些字段且无需更新。 如果我们为这两个字段选择了值类型而不是指针类型,如果它们不存在,那么这些字段的对应零值将为 0 false ,我们将无法确定它们是否实际设置__。

文件更新方法如下。

  1. func (fh fileHandler) updateFile(f file) error {
  2. var query []string
  3. var param []interface{}
  4. if f.offset != nil {
  5. of := fmt.Sprintf("file_offset = $1")
  6. ofp := f.offset
  7. query = append(query, of)
  8. param = append(param, ofp)
  9. }
  10. if f.uploadComplete != nil {
  11. uc := fmt.Sprintf("file_upload_complete = $2")
  12. ucp := f.uploadComplete
  13. query = append(query, uc)
  14. param = append(param, ucp)
  15. }
  16. if len(query) > 0 {
  17. mo := "modified_at = $3"
  18. mop := "NOW()"
  19. query = append(query, mo)
  20. param = append(param, mop)
  21. qj := strings.Join(query, ",")
  22. sqlq := fmt.Sprintf("UPDATE file SET %s WHERE file_id = $4", qj)
  23. param = append(param, f.fileID)
  24. log.Println("generated update query", sqlq)
  25. _, err := fh.db.Exec(sqlq, param...)
  26. if err != nil {
  27. log.Println("Error during file update", err)
  28. return err
  29. }
  30. }
  31. return nil
  32. }

让我简要说明这种方法的工作原理。我们在第 2 3 行中定义了两个切片 queryparam 。我们将更新查询附加到 query 切片和 params 切片中的相应参数。最后,我们将使用这两个切片的内容创建更新查询。

第 4 行我们检查偏移量是否为 nil。如果不是,我们将相应的更新语句添加到 query 切片和 param 切片的参数。我们对第10行中的 uploadComplete 应用了类似的逻辑。

第 17 行,我们检查 query 的长度是否大于零。如果是,则表示我们有一个要更新的字段。第 18 行,然后我们添加查询和字段来更新 modified_at DB字段。

第 24 行连接 query 切片的内容以创建查询。

让我们尝试使用 fileID 32, offset 100 and uploadComplete falsefile 结构来更好地理解此代码。

第 17 行的 queryparam 切片的内容。

  1. query = []string{"file_offset = $1", "file_upload_complete = $2"}
  2. params = []interface{}{100, false}

第 30 行生成的更新查询将是表单

  1. UPDATE file SET file_offset = $1, file_upload_complete = $2,
  2. modified_at = $3 WHERE file_id = $4

最后的 param 切片将为 {100, false, NOW(), 32}

我们第 31 行执行查询并返回错误(如果有的话)。

获取文件

tus 协议所需的最终 DB 方法是一种在提供 fileID 时返回文件详细信息的方法。

  1. func (fh fileHandler) File(fileID string) (file, error) {
  2. fID, err := strconv.Atoi(fileID)
  3. if err != nil {
  4. log.Println("Unable to convert fileID to string", err)
  5. return file{}, err
  6. }
  7. log.Println("going to query for fileID", fID)
  8. gfstmt := `select file_id, file_offset, file_upload_length, file_upload_complete from file where file_id = $1`
  9. row := fh.db.QueryRow(gfstmt, fID)
  10. f := file{}
  11. err = row.Scan(&f.fileID, &f.offset, &f.uploadLength, &f.uploadComplete)
  12. if err != nil {
  13. log.Println("error while fetching file", err)
  14. return file{}, err
  15. }
  16. return f, nil
  17. }

在上面的方法中,我们在提供 fileID 时返回文件的详细信息。这是直截了当的。

现在我们已经完成了 DB 方法,下一步将是创建 http 处理程序。我们将在下一个教程中执行此操作。

原文链接

https://golangbot.com/resumable-file-uploader-implementing-db-crud-methods/