如何设计简单抖音的储存模块 | 青训营

208 阅读4分钟

如何设计简单抖音的储存模块| 青训营

我们的极简抖音项目地址 BakaDream/WheelChair-tiktok (github.com)

1.理清项目要求

  • 多种储存系统可选 (腾讯云cos,各种oss,本地存储)
  • 可通过配置文件进行配置
  • 可以生成视频封面
  • 丰富错误处理
  • return 视频 封面链接

2.配置文件设计

根据项目需求,我们队伍选择了使用环境变量来加载配置。我们使用了godotenv这个库,他有如下好处

  1. 可从本地配置文件加载
  2. 可以加载系统中的环境变量 以便打包docker 和在不同环境下使用 例如:测试环境 开发环境 生产环境
  3. 这个库轻,易于使用

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即可