可恢复的文件上传器-实现DB的CRUD方法

101 阅读4分钟

在本教程中,我们将创建数据模型和数据库CRUD方法。

本教程有以下几个部分

  • 数据模型
  • 表的创建
  • 图斯检索
  • 创建文件
  • 更新文件
  • 获取文件

数据模型

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

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

我们需要一个字段来唯一地识别文件。field_id 为了保持简单,我们将使用一个自动递增的integer 字段作为文件标识符。这个字段将是该表的主键。我们还将使用这个ID作为文件名。

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

一个布尔字段file_upload_complete ,用于确定整个文件是否被上传。

我们还将有通常的审计字段created_atmodified_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 结构代表一个文件。它的字段是不言自明的。我们为offsetuploadLength 选择指针类型是有原因的,后面会解释。

接下来我们将添加向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也被用作以后的文件名。

更新文件

现在我们来写一下文件更新方法。在一个典型的文件中,我们只需要更新文件的offsetuploadComplete 字段。一旦文件被创建,fileIDuploadLength 将不会改变。这也是我们在file 结构中为offsetuploadComplete 选择指针的原因。如果offsetuploadCompletenil ,这意味着这些字段没有被设置,不需要被更新。如果我们为这两个字段选择值类型而不是指针类型,如果它们不存在,这些字段仍然会有相应的零值0false ,我们将无法发现它们是否真的被设置。

下面提供了文件更新的方法。

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行定义了两个片断queryparam 。我们将把更新查询附加到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行,queryparam 分片的内容将被连接在一起。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处理程序。我们将在下一个教程中完成这一工作。