在本教程中,我们将创建数据模型和数据库CRUD方法。
本教程有以下几个部分
- 数据模型
- 表的创建
- 图斯检索
- 创建文件
- 更新文件
- 获取文件
数据模型
让我们首先讨论一下我们的tus服务器的数据模型。我们将使用PostgreSQL作为数据库。
我们的tus服务器需要一个表file 来存储与文件有关的信息。让我们讨论一下该表应该有哪些字段。
我们需要一个字段来唯一地识别文件。field_id 为了保持简单,我们将使用一个自动递增的integer 字段作为文件标识符。这个字段将是该表的主键。我们还将使用这个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 ,这是数据库的句柄。这将在稍后从main中注入。在第5行,我们添加了5行,我们添加了createTable() 方法。如果表不存在,该方法将创建该表,如果有错误,则返回错误。
Tus Recollection
在我们创建DB CRUD方法之前,让我们回顾一下tus协议使用的http方法
POST- 创建一个新文件
PATCH- 将数据上传到一个现有文件的偏移处Upload-Offset
HEAD- 获取文件的当前Upload-Offset ,以开始下一个补丁请求。
我们将需要创建、更新和读取表操作来支持上述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 转换为字符串并返回。这是很直接的。我们之所以要将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行中,我们检查offset是否为。nil如果不是,我们将相应的更新语句添加到query 片段,并将参数添加到param 片段。在第4行中,我们对uploadComplete 采用类似的逻辑。10.
在第10行中,我们对。17,我们检查query 的长度是否大于零。如果是真的,这意味着我们有一个字段需要更新。在第18行中,我们添加了查询,并将其作为 "新 "字段。18,我们添加查询和字段来更新modified_at DB字段。
第24行连接query slice的内容来创建查询。
让我们试着用file 结构和fileID 32, offset 100 and uploadComplete false 来更好地理解这段代码。
在第17行,query 和param 分片的内容将被连接在一起。17行的内容将是
query = []string{"file_offset = $1", "file_upload_complete = $2"}
params = []interface{}{100, false}
在第30行生成的更新查询将是第30行生成的更新查询将是这样的
UPDATE file SET file_offset = $1, file_upload_complete = $2, modified_at = $3 WHERE file_id = $4
而最后的param 片断将是{100, false, NOW(), 32}
我们在第31行执行查询。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处理程序。我们将在下一个教程中完成这一工作。