Golang仿云盘项目- 5. 用户上传/查询文件/秒传

秒传原理

TODO

服务架构变迁

image
较之前的加入了用户文件表、hash计算。

  • 唯一文件表:一个文件只存一条记录,文件的filesha1为主键
  • 用户文件表:存储每个用户所有文件的元数据
  • Hash计算:内潜在上传server里,作为内部逻辑模块存在;也可以单独抽出来作为独立的微服务,向外提供接口

无秒传的流程:

  1. 【用户】上传文件数据,【上传Server】接收到文件数据流后,
  2. 它会一边存储在【本地存储】,
  3. 一边通知【hash计算】模块,该模块计算当前上传那一块文件的哈希值,等到整个文件上传完成之后,就会得到整个文件的哈希值
  4. 然后【上传server】将其哈希值写入到【唯一文件表】;
  5. 最后【上传server】把文件元信息关联到【用户文件表】。
    后面用户就可以通过【用户文件表】查询相应文件元信息。

秒传的情况下:

  1. 没有实际的文件传输了,因为【上传Server】在比对哈希值后发现值相同后会直接忽略3,4步,直接跳到第5步(将文件元信息关联到用户文件表)

本节完成效果
image

本文来自博客园,作者:Jayvee,转载请注明原文链接: https://www.cnblogs.com/cenjw/p/16496050.html

用户上传文件

db/userfile.go

点击查看代码
package db

import (
	mydb "FileStorageDisk/db/mysql"
	"fmt"
	"time"
)

// UserFile: 与用户文件表字段对应
type UserFile struct {
	UserName string
	FileHash string
	FileName string
	FileSize int64
	UploadAt string
	LastUpdated string
}

// OnUserFileUploadFinished : 更新用户文件表
func OnUserFileUploadOK(username, filehash, filename string, filesize int64) bool {
	stmt, err := mydb.DBConn().Prepare(
		"insert ignore into tbl_user_file (user_name,file_sha1,file_name," +
			"file_size,upload_at) values (?,?,?,?,?)")
	if err != nil {
		return false
	}
	defer stmt.Close()

	_, err = stmt.Exec(username, filehash, filename, filesize, time.Now())
	if err != nil {
		return false
	}
	return true
}

// QueryUserFileMetas : 批量获取用户文件信息
func QueryUserFileMetas(username string, limit int) ([]UserFile, error) {
	stmt, err := mydb.DBConn().Prepare(
		"select file_sha1,file_name,file_size,upload_at," +
			"last_update from tbl_user_file where user_name=? limit ?")
	if err != nil {
		return nil, err
	}
	defer stmt.Close()

	rows, err := stmt.Query(username, limit)
	if err != nil {
		return nil, err
	}

	var userFiles []UserFile
	for rows.Next() {
		ufile := UserFile{}
		err = rows.Scan(&ufile.FileHash, &ufile.FileName, &ufile.FileSize,
			&ufile.UploadAt, &ufile.LastUpdated)
		if err != nil {
			fmt.Println(err.Error())
			break
		}
		userFiles = append(userFiles, ufile)
	}
	// fmt.Printf("userFiles2: %v\n", userFiles)
	return userFiles, nil
}

用户批量查询文件接口

handler/handler.go

点击查看代码
// QueryFileHandler: 查询批量文件信息
func FileQueryHandler(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()

	username := r.Form.Get("username")
	limitCnt, _ := strconv.Atoi(r.Form.Get("limit")) 
	userFiles, err := dblayer.QueryUserFileMetas(username, limitCnt)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	// fmt.Printf("userFiles: %v\n", userFiles)

	data, err := json.Marshal(userFiles)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	w.Write(data)
}


// UploadHandler: 文件上传接口
func UploadHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		// 返回上传的HTML页面
		data, err := ioutil.ReadFile("./static/view/index.html")
		if err != nil {
			fmt.Println("Internal server error")
			return
		}
		io.WriteString(w, string(data))

	} else if r.Method == "POST" {
		// 1.获取文件句柄、文件头、错误(如果有)
		file, header, err := r.FormFile("file")
		if err != nil {
			fmt.Printf("Failed to get data, err:%s\n", err.Error())
			return
		}
		defer file.Close()

		// 5. 设置文件元信息
		fileMeta := meta.FileMeta{
			FileName: header.Filename,
			Location: "/tmp/" + header.Filename,
			UploadAt: time.Now().Format("2006-01-02 15:04:05"),
		}

		// 2.创建一个本地文件接收当前文件流
		// newFile, err := os.Create("/tmp/" + header.Filename)
		newFile, err := os.Create(fileMeta.Location)
		if err != nil {
			fmt.Printf("Failed to create file, err:%s\n", err.Error())
			return
		}
		// 3. 将内存中的文件拷贝到newFile的buffer区
		// _, err = io.Copy(newFile, file)
		fileMeta.FileSize, err = io.Copy(newFile, file)
		if err != nil {
			fmt.Printf("Failed to save data into file, err:%s\n", err.Error())
			return
		}

		// 6. 更新FileMeta
		newFile.Seek(0, 0) // 把文件句柄的位置移到开始位置
		fileMeta.FileSha1 = util.FileSha1(newFile)
		// meta.UpdateFileMeta(fileMeta)
		// 持久化到数据库
		_ = meta.UpdateFileMetaToDB(fileMeta)
		r.ParseForm()
		username := r.Form.Get("username")
		ok := dblayer.OnUserFileUploadOK(username, fileMeta.FileSha1, fileMeta.FileName, fileMeta.FileSize)
		if ok {
			// 4. 向客户端返回成功信息/或重定向到一个成功页面
			// http.Redirect(w, r, "/file/upload/suc", http.StatusFound)
			http.Redirect(w, r, "/static/view/home.html", http.StatusFound)
		}else {
			w.Write([]byte("Upload Failed."))
		}
	}
}

秒传接口

handler/handler.go

点击查看代码
// TryFastUploadHandler:尝试秒传接口
func TryFastUploadHandler(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()

	// 1. 解析请求参数
	username := r.Form.Get("username")
	filehash := r.Form.Get("filehash")
	filename := r.Form.Get("filename")
	filesize, _ := strconv.Atoi(r.Form.Get("filesize"))

	// 2. 从文件表中查询相同hash的文件记录
	fileMeta, err := meta.GetFileMetaFromDB(filehash)
	// fmt.Printf("fileMeta: %v\n", fileMeta)
	if err != nil {
		fmt.Println(err.Error())
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	// 3. 查不到则返回秒传失败
	if fileMeta == nil {
		resp := util.RespMsg{
			Code: -1,
			Msg: "秒传失败,请访问普通上传接口",
		}
		w.Write(resp.JSONBytes())
		return
	}
	// 4. 上传过则将文件信息写入用户文件表,返回成功
	ok := dblayer.OnUserFileUploadOK(username, filehash, filename, int64(filesize))
	if ok {
		resp := util.RespMsg{
			Code: 0,
			Msg: "秒传成功!",
		}
		w.Write(resp.JSONBytes())
		return
	}

	resp := util.RespMsg{
		Code: -2,
		Msg: "秒传失败,请稍后重传!",
	}
	w.Write(resp.JSONBytes())
	return
}

修改表结构

alter table tbl_user_file drop index `idx_user_file`;

postman调试

image

image

posted @ 2022-07-20 21:58  micromatrix  阅读(75)  评论(0编辑  收藏  举报