如何设计简单抖音的储存模块| 青训营
我们的极简抖音项目地址 BakaDream/WheelChair-tiktok (github.com)
1.理清项目要求
- 多种储存系统可选 (腾讯云cos,各种oss,本地存储)
- 可通过配置文件进行配置
- 可以生成视频封面
- 丰富错误处理
- return 视频 封面链接
2.配置文件设计
根据项目需求,我们队伍选择了使用环境变量来加载配置。我们使用了godotenv这个库,他有如下好处
- 可从本地配置文件加载
- 可以加载系统中的环境变量 以便打包docker 和在不同环境下使用 例如:测试环境 开发环境 生产环境
- 这个库轻,易于使用
godotenv的安装与使用
godotenv的安装: go get github.com/joho/godotenv godotenv的使用:
我的想法是单独一个config模块,然后在main.go里直接调用config.Loadenvv()函数就行。
config.go如下所示:
package config
import (
"github.com/joho/godotenv"
"log"
)
func LoadEnv() {
err := godotenv.Load(".env")
if err != nil {
log.Fatal("Error loading environment file:", err)
}
}
在这里 我们可以看到,程序加载了.env这个文件,平时我们要用到变量值的时候,直接调用os.Getenv("Key")把Key换成对应的键就能取到你设置的值了。
下面我们讨论我们需要设置哪些环境变量。
首先我们需要一个STRANGE_TYPE来选择我们选择哪个储存系统他的值可以是Local,TencentCOS,QiniuOSS等等,在具体代码中我们使用工厂模式来匹配,下面我将举一个Local例子和腾讯云COS的例子。
Local
LOCAL_PATH="./public" #这个是静态文件的存放位置,用相对路径一旦确定就不要更改我是写死了
LOCAL_STATIC_URL="s.example.com" 就依照例子这样写这么写
TencentCOS
TENCENT_COS_URL="xxxxx.myqcloud.com" #储存桶url
TENCENT_COS_SECRET_ID="xxxxx" #ID和Key 建议用子账号限定权限 TENCENT_COS_SECRET_KEY="xxxxx"
注意! 要去储存桶的配置界面改为公有读,私有写 这样才能让用户访问。
3.具体代码实现
用来管理的代码如下所示
package storage
import (
"WheelChair-tiktok/logger"
"io"
"mime/multipart"
"os"
)
var Storage Store
type Store interface {
UploadFile(file io.Reader, filePath string) (string, error)
GetSnapshot(videoFile *multipart.FileHeader) (string, error)
}
func Init() {
switch os.Getenv("STORAGE_TYPE") {
case "TencentCOS":
Storage = &TencentCos{
CosUrl: os.Getenv("TENCENT_COS_URL"),
SecretId: os.Getenv("TENCENT_COS_SECRET_ID"),
SecretKey: os.Getenv("TENCENT_COS_SECRET_KEY"),
}
case "Local":
Storage = &Local{
Static: os.Getenv("LOCAL_STATIC_URL"),
}
default:
logger.Logger.Fatal("Storage type has some err")
}
}
在这篇代码中,我声明了一个Store接口,有上传文件和获取封面两个方法。 而Init函数呢,会根据你配置文件里设置的储存类型进行匹配,然后让Storage这个全局对象成为对应的对象。在别处使用这代码时我们就只需要调用storage.Storage这个全局对象的方法接口,而不用管他是如何实现的
下面是Local.go的代码
package storage
import (
"WheelChair-tiktok/utils"
"io"
"mime/multipart"
"os"
"path/filepath"
)
type Local struct {
Static string
}
func (l *Local) UploadFile(file io.Reader, filePath string) (string, error) {
dst := "public/" + filePath
if err := os.MkdirAll(filepath.Dir(dst), 0750); err != nil {
return "", err
}
// 创建目标文件以供写入
out, err := os.Create(dst)
if err != nil {
return "", err
}
defer out.Close() // 在函数结束时关闭文件
// 将上传文件的内容复制到目标文件
_, err = io.Copy(out, file)
if err != nil {
return "", err
}
return l.Static + "/" + dst, nil
}
func (l *Local) GetSnapshot(videoFile *multipart.FileHeader) (string, error) {
videoPath := "./public/" + "videos/" + videoFile.Filename
name := utils.GetBaseNameWithoutExtension(videoFile.Filename)
picName := name + ".jpg"
picPath := "./public/" + "cover/" + picName
if err := os.MkdirAll(filepath.Dir(picPath), 0750); err != nil {
return "", err
}
err := utils.GetFirstFrame(videoPath, picPath)
if err != nil {
return "", err
}
picUrl := l.Static + "/public/cover/" + picName
return picUrl, nil
}
这里我们必须先上传完视频后再去获取封面
下面啥tencent_cos.go的代码
package storage
import (
"WheelChair-tiktok/utils"
"context"
"github.com/tencentyun/cos-go-sdk-v5"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
)
type TencentCos struct {
CosUrl string
SecretId string
SecretKey string
}
// UploadFile 传入io.Reader ,filepath 返回云端路径 ,err 记得在外部要关闭io.Reader
func (c *TencentCos) UploadFile(file io.Reader, filePath string) (string, error) {
client := c.newClient()
_, err := client.Object.Put(context.Background(), filePath, file, nil)
if err != nil {
return "", err
}
fileUrl := c.CosUrl + "/" + filePath
return fileUrl, nil
}
func (c *TencentCos) newClient() *cos.Client {
u, _ := url.Parse(c.CosUrl)
baseURL := &cos.BaseURL{BucketURL: u}
// 1.永久密钥
client := cos.NewClient(baseURL, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: c.SecretId, // 用户的 SecretId,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参考 https://cloud.tencent.com/document/product/598/37140
SecretKey: c.SecretKey, // 用户的 SecretKey,建议使用子账号密钥,授权遵循最小权限指引,降低使用风险。子账号密钥获取可参考 https://cloud.tencent.com/document/product/598/37140
},
})
return client
}
func (c *TencentCos) GetSnapshot(videoFile *multipart.FileHeader) (string, error) {
name := utils.GetBaseNameWithoutExtension(videoFile.Filename)
picName := name + ".jpg"
picPath := "cover/" + picName
f, err := videoFile.Open()
if err != nil {
return "", err
}
defer f.Close()
// 创建临时视频文件
tempVideo, err := os.CreateTemp("", videoFile.Filename)
if err != nil {
// Handle ERROR
return "", err
}
defer os.Remove(tempVideo.Name()) // 清理临时文件
defer tempVideo.Close() // 关闭临时文件
// 将响应体写入临时文件
_, err = io.Copy(tempVideo, f)
if err != nil {
// Handle ERROR
return "", err
}
tempPic, err := os.CreateTemp("", picName)
if err != nil {
// Handle ERROR
return "", err
}
defer os.Remove(tempPic.Name()) // 清理临时文件
defer tempPic.Close()
//生成封面
utils.GetFirstFrame(tempVideo.Name(), tempPic.Name())
//上传临时文件
picUrl, err := c.UploadFile(tempPic, picPath)
if err != nil {
return "", err
}
return picUrl, nil
}
由于我们是把视频存在了云端,而不是本地,所以生成封面的时候我用到了临时文件,把视频先存在临时文件然后生成一个临时文件的封面,ffmpeg调用视频的临时文件,把封面覆写到临时文件封面里,然后把封面上传到oss里,最后返回值是拼接出对应的URl
下面是ffmpeg生成封面第具体逻辑代码
func GetFirstFrame(videoPath string, outputPath string) error {
cmd := exec.Command("ffmpeg", "-i", videoPath, "-f", "image2", "-t", "0.001", "-y", outputPath)
err := cmd.Run()
return err
}
我们传入视频路径 和输出路径 等ffmpeg获取封面且没出错后,直接上传或拼接出对应的url即可