上一个教程解释了 tus 协议如何工作。如果你是 tus 的新手,我强烈建议你阅读 上一个教程。在本教程中,我们将创建数据模型和数据库 CRUD 方法。
本教程包含以下部分
数据模型
表创建
Tus 回顾
生成文件
更新文件
获取文件
数据模型
我们先来讨论一下 tus 服务器的数据模型。 我们将使用 PostgreSQL 作为数据库。
我们的 tus 服务器需要一个表 file
来存储与文件相关的信息。 我们来讨论该表中应该包含哪些字段。
我们需要一个字段来唯一标识文件。 为了简单起见,我们将使用自动递增的 integer
字段 field_id
作为文件标识符。 该字段将是表的主键。 我们还将使用此 id 作为文件名。
接下来,我们的服务器需要跟踪每个文件的偏移量。 我们将使用 integer
字段field_offset
来存储文件偏移量。 我们将使用另一个 integer
字段file_upload_length
来存储文件的上传长度。
布尔字段 file_upload_complete
用于确定是否已上载整个文件。
我们还将拥有通常的审计字段 created_at
和 modified_at
这是表
file_id SERIAL PRIMARY KEY
file_offset INT NOT NULL
file_upload_length INT NOT NULL
file_upload_complete BOOLEAN NOT NULL
created_at TIMESTAMP default NOW() not null
modified_at TIMESTAMP default NOW() not null
表创建
我们将首先创建一个名为 fileserver
的数据库,然后编写代码来创建 file
表。
请使用以下命令切换到终端中的 psql
提示符
$ \psql -U postgres
系统将提示您输入密码。成功登录后,您可以查看 postgres 命令提示符。
postgres=# create database fileserver;
上面的命令将创建数据库 fileserver
现在我们已准备好数据库了,让我们继续并在代码中创建表。
type fileHandler struct {
db *sql.DB
}
func (fh fileHandler) createTable() error {
q := `CREATE TABLE IF NOT EXISTS file(file_id SERIAL PRIMARY KEY,
file_offset INT NOT NULL, file_upload_length INT NOT NULL, file_upload_complete BOOLEAN NOT NULL,
created_at TIMESTAMP default NOW() NOT NULL, modified_at TIMESTAMP default NOW() NOT NULL)`
_, err := fh.db.Exec(q)
if err != nil {
return err
}
log.Println("table create successfully")
return nil
}
我们有一个 fileHandler
结构,它包含一个字段 db
,它是数据库的句柄。这将在稍后从主要注入。在第5行我们添加了 createTable()
方法。如果表不存在,则此方法创建表,如果有则返回错误。
Tus协议回顾
在我们创建 DB CRUD 方法之前,让我们重新选择 tus 协议使用的 http 方法
POST - 创建新文件
PATCH - 将数据上传 Upload-Offset
到现有文件
HEAD - 获取到当前上传文件的 Upload-Offset
下载偏移量,以开始下个 patch 请求
我们将需要创建、更新和读取表操作来支持上述 http 方法。我们将在本教程中创建它们。
创建文件
在添加创建文件的方法之前,让我们先定义文件数据结构。
type file struct {
fileID int
offset *int
uploadLength int
uploadComplete *bool
}
上面的 file
结构表示一个文件。字段是一目了然的。我们为 offset
和uploadLength
选择指针类型是有原因的,稍后将对此进行解释。
接下来,我们将添加将新行插入 file
表的方法。
func (fh fileHandler) createFile(f file) (string, error) {
cfstmt := `INSERT INTO file(file_offset, file_upload_length, file_upload_complete) VALUES($1, $2, $3) RETURNING file_id`
fileID := 0
err := fh.db.QueryRow(cfstmt, f.offset, f.uploadLength, f.uploadComplete).Scan(&fileID)
if err != nil {
return "", err
}
fid := strconv.Itoa(fileID)
return fid, nil
}
上面的方法将一行插入到 file
表中,并将 fileID
转换为string并返回它。这是很简单的。我们将 fileID
转换为 string
的原因是,稍后还将使用fileID作为文件的名称。
更新文件
我们现在编写文件更新方法。 在典型的文件中,我们只需要更新文件的 offset
和uploadComplete
字段。 创建文件后,fileID
和 uploadLength
不会更改。 这也是我们在 file
结构中为 offset
和 uploadComplete
选择指针的原因。 如果 offset
或 uploadComplete
为 nil
,则表示未设置这些字段且无需更新。 如果我们为这两个字段选择了值类型而不是指针类型,如果它们不存在,那么这些字段的对应零值将为 0
和 false
,我们将无法确定它们是否实际设置__。
文件更新方法如下。
func (fh fileHandler) updateFile(f file) error {
var query []string
var param []interface{}
if f.offset != nil {
of := fmt.Sprintf("file_offset = $1")
ofp := f.offset
query = append(query, of)
param = append(param, ofp)
}
if f.uploadComplete != nil {
uc := fmt.Sprintf("file_upload_complete = $2")
ucp := f.uploadComplete
query = append(query, uc)
param = append(param, ucp)
}
if len(query) > 0 {
mo := "modified_at = $3"
mop := "NOW()"
query = append(query, mo)
param = append(param, mop)
qj := strings.Join(query, ",")
sqlq := fmt.Sprintf("UPDATE file SET %s WHERE file_id = $4", qj)
param = append(param, f.fileID)
log.Println("generated update query", sqlq)
_, err := fh.db.Exec(sqlq, param...)
if err != nil {
log.Println("Error during file update", err)
return err
}
}
return nil
}
让我简要说明这种方法的工作原理。我们在第 2 3 行中定义了两个切片 query
和 param
。我们将更新查询附加到 query
切片和 params
切片中的相应参数。最后,我们将使用这两个切片的内容创建更新查询。
第 4 行我们检查偏移量是否为 nil
。如果不是,我们将相应的更新语句添加到 query
切片和 param
切片的参数。我们对第10行中的 uploadComplete
应用了类似的逻辑。
第 17 行,我们检查 query
的长度是否大于零。如果是,则表示我们有一个要更新的字段。第 18 行,然后我们添加查询和字段来更新 modified_at
DB字段。
第 24 行连接 query
切片的内容以创建查询。
让我们尝试使用 fileID 32, offset 100 and uploadComplete false
的 file
结构来更好地理解此代码。
第 17 行的 query
和 param
切片的内容。
query = []string{"file_offset = $1", "file_upload_complete = $2"}
params = []interface{}{100, false}
第 30 行生成的更新查询将是表单
UPDATE file SET file_offset = $1, file_upload_complete = $2,
modified_at = $3 WHERE file_id = $4
最后的 param
切片将为 {100, false, NOW(), 32}
我们第 31 行执行查询并返回错误(如果有的话)。
获取文件
tus 协议所需的最终 DB 方法是一种在提供 fileID
时返回文件详细信息的方法。
func (fh fileHandler) File(fileID string) (file, error) {
fID, err := strconv.Atoi(fileID)
if err != nil {
log.Println("Unable to convert fileID to string", err)
return file{}, err
}
log.Println("going to query for fileID", fID)
gfstmt := `select file_id, file_offset, file_upload_length, file_upload_complete from file where file_id = $1`
row := fh.db.QueryRow(gfstmt, fID)
f := file{}
err = row.Scan(&f.fileID, &f.offset, &f.uploadLength, &f.uploadComplete)
if err != nil {
log.Println("error while fetching file", err)
return file{}, err
}
return f, nil
}
在上面的方法中,我们在提供 fileID
时返回文件的详细信息。这是直截了当的。
现在我们已经完成了 DB 方法,下一步将是创建 http 处理程序。我们将在下一个教程中执行此操作。
原文链接
https://golangbot.com/resumable-file-uploader-implementing-db-crud-methods/