Softhub軟件下載站實戰開發(十):實現圖片視頻上傳下載接口

文章目錄

  • Softhub軟件下載站實戰開發(十):實現圖片視頻上傳下載接口 🖼?🎥
    • 系統架構圖
    • 核心功能設計 🛠?
      • 1. 文件上傳流程
      • 2. 關鍵技術實現
        • 2.1 雪花算法
        • 2.2 文件校驗機制 ?
        • 2.3 文件去重機制 🔍
        • 2.4 視頻封面提取 🎞?
        • 2.5 文件存儲策略 📂
        • 2.6 視頻上傳示例
      • 3. 文件查看實現 ??

Softhub軟件下載站實戰開發(十):實現圖片視頻上傳下載接口 🖼?🎥

在上一篇文章中,我們實現了軟件配置面板,實現了ai配置信息的存儲,為后續富文本編輯器的ai功能提供了基礎,本文致力于解決在富文本編輯器中圖片和視頻的上傳查看功能。

系統架構圖

上傳文件
下載文件
讀取
客戶端
API接口
文件處理層
存儲服務
MinIO存儲
數據庫
MySQL

核心功能設計 🛠?

1. 文件上傳流程

客戶端 服務端 MinIO 數據庫 上傳文件請求 驗證文件類型和大小 計算文件MD5 檢查文件是否已存在 返回已存在記錄 直接返回文件URL 上傳文件到MinIO 返回成功 保存文件元信息 返回成功 返回文件URL alt [文件已存在] [文件不存在] 客戶端 服務端 MinIO 數據庫

2. 關鍵技術實現

2.1 雪花算法

關鍵數據不能采取自增id方案,采用md5也會有碰撞和頁分裂的問題,這里采用雪花算法來解決這一問題

安裝

go get -u "github.com/bwmarrin/snowflake"

初始化

var node *snowflake.Nodefunc init() {var err errornode, err = snowflake.NewNode(1)
}

使用

id := node.Generate().Int64()
2.2 文件校驗機制 ?
// 檢查文件類型
fileType := strings.ToLower(filepath.Ext(req.File.Filename))
allowedTypes := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
isAllowed := false
for _, t := range allowedTypes {if t == fileType {isAllowed = truebreak}
}
if !isAllowed {return fmt.Errorf("不支持的文件類型:%s", fileType)
}// 檢查文件大小
if req.File.Size > 10*1024*1024 { // 10MBreturn fmt.Errorf("文件大小不能超過10MB")
}
2.3 文件去重機制 🔍

通過計算文件MD5值實現文件去重:

// 計算文件MD5
fileBytes, _ := io.ReadAll(file)
md5 := gmd5.MustEncryptBytes(fileBytes)// 檢查是否已存在
var existFile *model.DsImageInfo
err = dao.DsImage.Ctx(ctx).Where(dao.DsImage.Columns().Md5, md5).Scan(&existFile)
if existFile != nil {// 直接返回已有文件信息return existFile, nil
}
2.4 視頻封面提取 🎞?

需要ffmpeg添加到環境變量中

使用FFmpeg提取視頻首幀作為封面:

cmd := exec.Command("ffmpeg","-y",                 // 覆蓋輸出文件"-loglevel", "error", // 只輸出錯誤信息"-i", tempVideoPath,  // 輸入文件"-vframes", "1",      // 只提取一幀"-an",                // 不處理音頻"-vf", "scale='-1:min(720,ih)'", // 限制最大高度為720"-c:v", "mjpeg",      // 使用mjpeg編碼器"-f", "image2",       // 輸出格式"-q:v", "2",          // 高質量輸出tempFramePath)        // 輸出文件
2.5 文件存儲策略 📂

采用分層目錄結構存儲文件:

pic/2024/05/07/abc123def456.pic
video/2024/05/07/xyz789uvw012.video

代碼實現:

now := gtime.Now()
year := now.Year()
month := int(now.Month())
day := now.Day()
objectName := fmt.Sprintf("pic/%d/%02d/%02d/%s.pic", year, month, day, md5)
2.6 視頻上傳示例
func (s *sDsIUpload) VideoUpload(ctx context.Context, req *api.DsVideoUploadReq) (res *api.DsVideoUploadRes, err error) {res = &api.DsVideoUploadRes{}err = g.Try(ctx, func(ctx context.Context) {// 檢查文件類型fileType := strings.ToLower(filepath.Ext(req.File.Filename))allowedTypes := []string{".mp4", ".avi", ".mov", ".mkv"}isAllowed := falsefor _, t := range allowedTypes {if t == fileType {isAllowed = truebreak}}if !isAllowed {liberr.ErrIsNil(ctx, fmt.Errorf("不支持的文件類型:%s", fileType))}// 檢查文件大小(如限制20MB)if req.File.Size > 20*1024*1024 {liberr.ErrIsNil(ctx, fmt.Errorf("文件大小不能超過20MB"))}// 計算MD5file, err := req.File.Open()liberr.ErrIsNil(ctx, err, "打開文件失敗")defer file.Close()fileBytes, err := io.ReadAll(file)liberr.ErrIsNil(ctx, err, "讀取文件失敗")md5 := gmd5.MustEncryptBytes(fileBytes)// 檢查是否已存在var existVideo *model.DsVideoInfoerr = dao.DsVideo.Ctx(ctx).Where(dao.DsVideo.Columns().Md5, md5).Scan(&existVideo)liberr.ErrIsNil(ctx, err, "查詢視頻信息失敗")if existVideo != nil {res.Id = existVideo.Idres.Url = fmt.Sprintf("/api/v1/admin/ds/dsVideo/view?id=%d", existVideo.Id)// 獲取首幀圖片URLimageInfo, err := s.GetImageInfo(ctx, &api.DsImageInfoReq{Id: existVideo.PosterId})if err == nil && imageInfo != nil {res.Poster = fmt.Sprintf("/api/v1/admin/ds/dsImage/view?id=%d", imageInfo.Id)}return}// 創建臨時目錄tempDir := filepath.Join(os.TempDir(), "upload", md5)if _, err := os.Stat(tempDir); os.IsNotExist(err) {err = os.MkdirAll(tempDir, 0755)liberr.ErrIsNil(ctx, err, "創建臨時目錄失敗")}// 生成臨時文件路徑tempVideoPath := filepath.Join(tempDir, fmt.Sprintf("video%s", fileType))tempFramePath := filepath.Join(tempDir, "frame.jpg")g.Log().Debugf(ctx, "臨時視頻文件路徑: %s", tempVideoPath)g.Log().Debugf(ctx, "臨時幀圖片路徑: %s", tempFramePath)// 保存視頻到臨時文件file.Seek(0, 0)tempFile, err := os.OpenFile(tempVideoPath, os.O_WRONLY|os.O_CREATE, 0644)liberr.ErrIsNil(ctx, err, "創建臨時文件失敗")_, err = io.Copy(tempFile, file)tempFile.Close()liberr.ErrIsNil(ctx, err, "保存臨時文件失敗")// 確保臨時文件存在且可讀if _, err := os.Stat(tempVideoPath); err != nil {liberr.ErrIsNil(ctx, fmt.Errorf("臨時視頻文件不存在或無法訪問: %v", err))}// 使用ffmpeg提取首幀cmd := exec.Command("ffmpeg","-y",                 // 覆蓋輸出文件"-loglevel", "error", // 只輸出錯誤信息"-i", tempVideoPath, // 輸入文件"-vframes", "1", // 只提取一幀"-an",                           // 不處理音頻"-vf", "scale='-1:min(720,ih)'", // 限制最大高度為720,保持寬高比"-c:v", "mjpeg", // 使用 mjpeg 編碼器"-f", "image2", // 輸出格式"-q:v", "2", // 高質量輸出tempFramePath) // 輸出文件output, err := cmd.CombinedOutput()if err != nil {// 清理臨時文件os.RemoveAll(tempDir)liberr.ErrIsNil(ctx, fmt.Errorf("提取視頻首幀失敗: %v, 輸出: %s", err, string(output)))}// 獲取MinIO客戶端drive := storage.MinioDrive{}client, err := drive.GetClient()liberr.ErrIsNil(ctx, err, "獲取MinIO客戶端失敗")// 生成存儲路徑now := gtime.Now()year := now.Year()month := int(now.Month())day := now.Day()frameObjectName := fmt.Sprintf("pic/%d/%02d/%02d/%s.jpg", year, month, day, md5)// 讀取首幀圖片frameFile, err := os.Open(tempFramePath)liberr.ErrIsNil(ctx, err, "打開首幀圖片失敗")defer frameFile.Close()// 獲取首幀圖片信息frameInfo, err := frameFile.Stat()liberr.ErrIsNil(ctx, err, "獲取首幀圖片信息失敗")// 檢查是否已存在相同MD5的圖片var existingImage *model.DsImageInfoerr = dao.DsImage.Ctx(ctx).Where(dao.DsImage.Columns().Md5, md5).Scan(&existingImage)liberr.ErrIsNil(ctx, err, "查詢圖片信息失敗")var imageId int64if existingImage != nil {// 使用已存在的圖片記錄imageId = existingImage.Id} else {// 獲取圖片尺寸frameFile.Seek(0, 0)img, _, err := image.DecodeConfig(frameFile)if err != nil {g.Log().Warningf(ctx, "獲取圖片尺寸失敗: %v", err)}// 重新定位到文件開始位置用于上傳frameFile.Seek(0, 0)// 上傳首幀圖片到MinIO_, err = client.PutObject(ctx, config.MINIO_BUCKET, frameObjectName, frameFile, frameInfo.Size(), minio.PutObjectOptions{ContentType: "image/jpeg",})liberr.ErrIsNil(ctx, err, "上傳首幀圖片失敗")// 保存首幀圖片信息imageInfo := &model.DsImageInfo{Id:        node.Generate().Int64(),Md5:       md5,Name:      fmt.Sprintf("%s_frame.jpg", req.File.Filename),Path:      frameObjectName,Size:      frameInfo.Size(),MimeType:  "image/jpeg",Width:     img.Width,Height:    img.Height,CreatedBy: 0,CreatedAt: gtime.Now(),UpdatedBy: 0,UpdatedAt: gtime.Now(),}// 保存首幀圖片信息到數據庫_, err = dao.DsImage.Ctx(ctx).Insert(imageInfo)liberr.ErrIsNil(ctx, err, "保存首幀圖片信息失敗")imageId = imageInfo.Id}// 獲取視頻元數據cmd = exec.Command("ffprobe","-v", "quiet","-print_format", "json","-show_format","-show_streams",tempVideoPath)output, err = cmd.Output()liberr.ErrIsNil(ctx, err, "獲取視頻信息失敗")var probeData struct {Streams []struct {Width    int    `json:"width"`Height   int    `json:"height"`Duration string `json:"duration"`} `json:"streams"`}err = json.Unmarshal(output, &probeData)liberr.ErrIsNil(ctx, err, "解析視頻信息失敗")width := 0height := 0duration := 0if len(probeData.Streams) > 0 {width = probeData.Streams[0].Widthheight = probeData.Streams[0].Heightif d, err := strconv.ParseFloat(probeData.Streams[0].Duration, 64); err == nil {duration = int(d)}}// 保存視頻文件到MinIOvideoObjectName := fmt.Sprintf("video/%d/%02d/%02d/%s.video", year, month, day, md5)file.Seek(0, 0)err = drive.UploadWithPath(ctx, req.File, videoObjectName)liberr.ErrIsNil(ctx, err, "保存文件失敗")// 保存視頻信息videoInfo := &model.DsVideoInfo{Id:        node.Generate().Int64(),PosterId:  imageId,Md5:       md5,Name:      req.File.Filename,Path:      videoObjectName,Size:      req.File.Size,MimeType:  req.File.Header.Get("Content-Type"),Duration:  duration,Width:     width,Height:    height,CreatedBy: 0,CreatedAt: gtime.Now(),UpdatedBy: 0,UpdatedAt: gtime.Now(),}_, err = dao.DsVideo.Ctx(ctx).Insert(videoInfo)liberr.ErrIsNil(ctx, err, "保存視頻信息失敗")// 清理臨時目錄os.RemoveAll(tempDir)res.Id = videoInfo.Idres.Url = fmt.Sprintf("/api/v1/admin/ds/dsVideo/view?id=%d", videoInfo.Id)res.Poster = fmt.Sprintf("/api/v1/admin/ds/dsImage/view?id=%d", imageId)})return
}

3. 文件查看實現 ??

獲取文件信息:返回JSON格式的元數據,前端根據返回的路徑進行接口請求

以視頻為例

// GetVideoInfo 獲取視頻信息
func (c *dsUploadController) GetVideoInfo(ctx context.Context, req *api.DsVideoInfoReq) (res *api.DsVideoInfoRes, err error) {// 查詢視頻信息videoInfo, err := service.DsUpload().GetVideoInfo(ctx, req)if err != nil {return nil, err}// 直接從 MinIO 讀取視頻內容drive := storage.MinioDrive{}client, err := drive.GetClient()if err != nil {return nil, err}obj, err := client.GetObject(ctx, config.MINIO_BUCKET, videoInfo.Path, minio.GetObjectOptions{})if err != nil {return nil, err}defer obj.Close()// 設置響應頭writer := g.RequestFromCtx(ctx).Response.ResponseWriterwriter.Header().Set("Content-Type", videoInfo.MimeType)writer.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", videoInfo.Name))// 寫入視頻流_, err = io.Copy(writer, obj)return nil, err // 不返回JSON
}// ViewVideo 返回視頻二進制流
func (c *dsUploadController) ViewVideo(ctx context.Context, req *api.DsVideoViewReq) (res *api.DsVideoViewRes, err error) {// 查詢視頻信息videoInfo, err := service.DsUpload().GetVideoInfo(ctx, &api.DsVideoInfoReq{Id: req.Id})if err != nil {return nil, err}// 直接從 MinIO 讀取視頻內容drive := storage.MinioDrive{}client, err := drive.GetClient()if err != nil {return nil, err}obj, err := client.GetObject(ctx, config.MINIO_BUCKET, videoInfo.Path, minio.GetObjectOptions{})if err != nil {return nil, err}defer obj.Close()// 設置響應頭writer := g.RequestFromCtx(ctx).Response.ResponseWriterwriter.Header().Set("Content-Type", videoInfo.MimeType)writer.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", videoInfo.Name))// 寫入視頻流_, err = io.Copy(writer, obj)return nil, err // 不返回JSON
}

softhub系列往期文章

  1. Softhub軟件下載站實戰開發(一):項目總覽
  2. Softhub軟件下載站實戰開發(二):項目基礎框架搭建
  3. Softhub軟件下載站實戰開發(三):平臺管理模塊實戰
  4. Softhub軟件下載站實戰開發(四):代碼生成器設計與實現
  5. Softhub軟件下載站實戰開發(五):分類模塊實現
  6. Softhub軟件下載站實戰開發(六):軟件配置面板實現
  7. Softhub軟件下載站實戰開發(七):集成MinIO實現文件存儲功能
  8. Softhub軟件下載站實戰開發(八):編寫軟件后臺管理
  9. Softhub軟件下載站實戰開發(九):編寫軟件配置管理界面

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/87147.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/87147.shtml
英文地址,請注明出處:http://en.pswp.cn/web/87147.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

[JS逆向] 喜馬拉雅登錄案例 -- 補環境

博客配套代碼發布于github:喜馬拉雅登錄 (歡迎順手Star一下?) 相關知識點:webpack 補環境 相關爬蟲專欄:JS逆向爬蟲實戰 爬蟲知識點合集 爬蟲實戰案例 逆向知識點合集 此案例目標為逆向成功對應的參數&#xff0c…

大語言模型推理系統綜述

摘要 近年來,隨著 ChatGPT 等服務推動大語言模型(LLM)的快速普及,一批專門面向 LLM 推理的系統相繼涌現,如 vLLM、SGLang、Mooncake 和 DeepFlow。這些系統設計工作的核心動因是 LLM 請求處理過程中所特有的自回歸特性…

用Firecrawl輕松獲取網站數據,提升AI應用的效率!

🔥 Firecrawl:助力AI應用的強大工具! 在數字化信息爆炸的時代,如何高效地從海量網頁中提取有用數據變得尤其重要。Firecrawl的問世,為我們揭開了一種便捷的方法來應對這一挑戰。它不僅能夠將整個網站的數據轉化為適用…

【王陽明代數講義】谷歌編程智能體Gemini CLI 使用指南、架構詳解與核心框架分析

Gemini CLI 使用指南、架構詳解與核心框架分析 Gemini CLI 使用指南、架構詳解與核心框架分析Gemini CLI 使用指南Gemini CLI 架構詳解Gemini CLI 核心框架總結 Gemini CLI 使用指南、架構詳解與核心框架分析 Gemini CLI 使用指南 1. 安裝與配置 環境要求: Node.…

camera調試:安卓添加xml注冊

對接安卓的平臺時,需要注冊對應的camera設備,供安卓標準api進行操作,rk的平臺需要在HAL層配置camera3_profiles.xml文件,適配驅動的信息,進行注冊camera設備。該xml對應的內容很多,很多CTS測試問題都是該文…

使用 Ansys Discovery 為初學者準備幾何結構

介紹 設計幾何體通常會包含一些特征,使其無法直接導入我們的仿真工具,例如 Ansys Mechanical、LS-DYNA、Fluent 等。有些干擾或錯位雖然適合制造,但在我們的仿真工具中卻會造成問題。有時,一些小特征(例如孔或圓角&am…

推客系統全棧開發指南:從架構設計到商業化落地

一、推客系統概述 推客系統(TuiKe System)是一種結合社交網絡與內容分發的創新型平臺,旨在通過用戶間的相互推薦機制實現內容的高效傳播。這類系統通常包含用戶關系管理、內容發布、智能推薦、數據分析等核心模塊,廣泛應用于電商…

大數據開發實戰:如何做企業級的數據服務產品

1.背景 數據服務通常以解決方案的形式進行組織,面向一個應用場景的所有數據需求或數據內容可以通過一個解決方案進行封裝,統一對外服務。一個數據需求或數據接口以一個數據服務實例的形式存在于解決方案之下。 下游消費方可以通過統一API進行數據消費&…

基于IndexTTS的零樣本語音合成

IndexTTS 項目采用模塊化設計,將 BPE 文本編碼、GPT 單元預測、dVAE 語音特征抽取和 BigVGAN 音頻生成串聯為完整的語音合成流程。系統通過統一的配置文件和模型目錄規范,實現高效的文本到語音轉換,支持命令行與 Web 界面雙模式操作&#xff…

基于go-zero的短鏈生成系統

go-zero框架 gozero(又稱go-zero)是一款由知名開發者kevwan設計的Golang微服務框架,專注于高性能、低延遲和易用性。其核心目標是簡化分布式系統的開發,提供開箱即用的工具鏈,涵蓋API網關、RPC服務、緩存管理、數據庫…

Linux-修改線上MariaDB服務端口號

準備工作(很重要!!!): 提前做好Linux服務器快照 提前做好數據庫數據備份 1. 修改配置文件 首先,我們需要找到MariaDB的配置文件。通常情況下,這個文件位于以下位置:…

Spring Cloud 微服務(負載均衡策略深度解析)

📌 摘要 在微服務架構中,負載均衡是實現高可用、高性能服務調用的關鍵機制之一。Spring Cloud 提供了基于客戶端的負載均衡組件 Ribbon,結合 Feign 和 OpenFeign,實現了服務間的智能路由與流量分配。 本文將深入講解 Spring Clo…

HTML/CSS基礎

1.html:超文本標記語言。它是一種標識性的語言,非編程語言,不能使用邏輯運算。通過標簽將網絡上的文本格式進行統一,使用分散網絡資源鏈接為一個邏輯整體,屬于標記語言。 超文本:就是指頁面內可以包含圖片&#xff0…

C# 事件驅動編程的核心:深度解析發布者_訂閱者模式

適用場景:GUI交互、消息隊列、微服務通信等需要解耦事件生產與消費的系統 🧩 模式核心組件解析 發布者(Publisher) 作用:定義事件并管理訂閱者列表關鍵行為: 提供和-運算符注冊/注銷訂閱者通過Invoke()方…

華為云Flexus+DeepSeek征文 | 從零開始搭建Dify-LLM應用開發平臺:華為云全流程單機部署實戰教程

華為云FlexusDeepSeek征文 | 從零開始搭建Dify-LLM應用開發平臺:華為云全流程單機部署實戰教程 前言一、華為云Dify-LLM平臺介紹1. Dify-LLM解決方案簡介2. Dify-LLM解決方案地址3. Dify-LLM單機架構介紹4. 預估成本說明 二、華為云Maas平臺介紹1. 華為云ModelArts …

oracle集合三嵌套表(Nested Table)學習

嵌套表 嵌套表(Nested Table)是Oracle中的一種集合數據類型,它允許在表中存儲多值屬性,類似于在表中嵌套另一個表。 嵌套表具有以下特點: 是Oracle對象關系特性的一部分 可以看作是一維數組,沒有最大元素數量限制 存儲在單獨…

Python學習之——單例模式

Python學習之——單例模式 參考1 利用__metaclass__實現單例super的用法class Singleton(type)元類 2 重載__new__方法實現單例模式3 利用裝飾器實現單例考慮一個類如果繼承一個單例類的問題 參考 python之metaclasssingleton(一) python之metaclasssin…

【Linux】U-boot常用命令總結

U-Boot 是嵌入式系統中常用的引導加載程序(bootloader),它提供了一套命令行接口,用于調試、加載操作系統鏡像以及進行硬件測試等操作。 1、變量操作命令 這些命令用于管理 U-Boot 的環境變量。 命令功能說明setenv name value設…

【Linux】不小心又創建了一個root權限賬戶,怎么將它刪除?!

一.前言 今天在學習linux提權的時候,把新建的一個普通賬戶權限提升成了root, 當我練習完提權,想要把這個賬戶刪掉的時候。 發現… 好家伙,這個根本刪不掉 隨后試了各種各樣的方法,都不行,后來突然想到是否…

數據結構:數組(Array)

目錄 什么是數組(Array)? 🔍為什么數組的下標要從 0 開始? 一、內存地址與偏移量的關系:從 0 開始是最自然的映射 二、指針的起點就是第 0 個元素的地址 三、歷史原因:BCPL → B → C → …