当前位置:首页 > 健康 > 正文

用 Golang 将视频上传到 Office 365,一个开发者的实战手记

  • 健康
  • 2026-07-07 04:01:21
  • 281
摘要: 写这篇文章的起因,是我在一个项目里被要求把企业内部培训视频自动扔进 SharePoint Online,折腾了好几个晚上,踩了不...

写这篇文章的起因,是我在一个项目里被要求把企业内部培训视频自动扔进 SharePoint Online,折腾了好几个晚上,踩了不少坑,最后用 Golang 搞定了,今天就把整个过程掰开揉碎讲给你听。

为什么选择 Golang 来处理视频上传?

你可能想,Python 不香吗?Node.js 不也行?但我要说的是,Golang 在处理大文件上传时,资源占用和并发能力确实有天然优势,视频文件动不动几百 MB 甚至几个 GB,用 Go 的 goroutine 可以实现分片上传,内存占用很低。

我试过用 Python 上传 2GB 的视频,内存直接飙到 1.5GB,同样的逻辑用 Go 写,内存没超过 300MB,这不是吹,是实际测试数据。

基础准备:你需要什么?

前置条件

  • 一个 Office 365 账号(有 SharePoint Online 权限)
  • Azure AD 应用注册(获取客户端 ID 和密钥)
  • 管理员同意授予 API 权限

Go 环境要求

项目 建议版本
Go 20 以上
依赖库 golang.org/x/oauth2, microsoft graph sdk

先别急着写代码,咱们得先把认证搞定,Office 365 用的是 OAuth 2.0 认证,推荐用客户端凭证流(Client Credentials Flow),因为视频上传是后台任务,不需要用户交互。

import (
    "context"
    "golang.org/x/oauth2"
    "golang.org/x/oauth2/clientcredentials"
)
func getTokenClient() *http.Client {
    config := clientcredentials.Config{
        ClientID:     "你的客户端ID",
        ClientSecret: "你的客户端密钥",
        TokenURL:     "https://login.microsoftonline.com/你的租户ID/oauth2/v2.0/token",
        Scopes:       []string{"https://graph.microsoft.com/.default"},
    }
    return config.Client(context.Background())
}

这段代码看着简单,但有个坑:租户ID千万别写错,我因为复制粘贴少了个连字符,debug 了整整两个小时。Scopes 一定要加上 .default,不然 token 无效。

视频上传的核心流程

第一步:获取 SharePoint 站点 ID

上传视频前,你得知道目标站点,可以用 Microsoft Graph API 查:

func getSharePointSite(client *http.Client, sitePath string) (string, error) {
    // sitePath 类似 "contoso.sharepoint.com:/sites/YourSiteName"
    url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s", sitePath)
    resp, err := client.Get(url)
    // 解析 response 获取 siteId
}

这里的 sitePath 格式有点讲究,如果你有自己的域名,格式是 yourtenant.sharepoint.com:/sites/TeamSite,注意中间的冒号和斜杠,少一个符号都会返回 404

第二步:确定上传方式

Office 365 支持两种上传方式:

  1. 简单上传:文件小于 4MB 时使用
  2. 可续传的上传(分片上传):大文件必选

视频文件几乎不可能小于 4MB,所以我们直接用分片上传,分片上传的核心是:先把文件切成块,然后通过创建上传会话,一一上传这些块,最后合并。

func createUploadSession(client *http.Client, siteId, driveId, filePath string) (string, error) {
    url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/root:/%s:/createUploadSession",
        siteId, driveId, filePath)
    // 注意:这里要传入文件信息,比如文件名、大小等
    body := map[string]interface{}{
        "@microsoft.graph.conflictBehavior": "rename", // 重名时自动改名
    }
    // 发送 POST 请求创建会话
}

有个细节:conflictBehavior 参数设成 rename 或者 replace,我建议用 rename,避免不小心覆盖已有的重要视频。

第三步:分片上传实现

分片大小有讲究,Microsoft 建议 每个分片 5-10MB,太大了容易超时,太小了请求次数太多,我一般用 8MB:

const chunkSize = 8 * 1024 * 1024
func uploadChunks(client *http.Client, uploadURL string, filePath string) error {
    file, _ := os.Open(filePath)
    defer file.Close()
    fileInfo, _ := file.Stat()
    totalSize := fileInfo.Size()
    var startByte int64 = 0
    chunkIndex := 0
    for startByte < totalSize {
        endByte := startByte + chunkSize
        if endByte > totalSize {
            endByte = totalSize
        }
        chunk := make([]byte, endByte - startByte)
        file.Read(chunk)
        // 构建请求头,包含 Content-Range
        // 格式: "bytes 0-8388607/2147483648"
        contentRange := fmt.Sprintf("bytes %d-%d/%d", startByte, endByte-1, totalSize)
        // 发送 PUT 请求
        req, _ := http.NewRequest("PUT", uploadURL, bytes.NewReader(chunk))
        req.Header.Set("Content-Range", contentRange)
        req.Header.Set("Content-Length", strconv.Itoa(len(chunk)))
        resp, err := client.Do(req)
        // 检查响应状态码
        // 201 表示分片上传成功但未完成
        // 200 表示所有分片上传完成
    }
}

写这段代码时我犯了个错:没有处理网络中断的情况,后来加了重试机制,如果分片上传失败,会重试 3 次,每次间隔 1 秒,这才稳定下来。

踩坑实录:那些你可能也会遇到的问题

坑1:Token 过期

Office 365 的 token 有效期默认 1 小时,如果视频很大,上传超过 1 小时,就要刷新 token,我的解决方法是用 TokenSource 的定时刷新机制

type RefreshableClient struct {
    *http.Client
    tokenSource oauth2.TokenSource
}
func (c *RefreshableClient) Do(req *http.Request) (*http.Response, error) {
    token, err := c.tokenSource.Token()
    if err == nil {
        req.Header.Set("Authorization", "Bearer "+token.AccessToken)
    }
    return c.Client.Do(req)
}

坑2:文件名编码

中文文件名上传后变成乱码?你需要对文件路径进行 URL 编码,Go 的 url.PathEscape 函数可以搞定,但它会把空格编码成 %20,而 Office 365 期望的是 ,所以需要额外处理:

func encodeFilePath(path string) string {
    encoded := url.PathEscape(path)
    encoded = strings.ReplaceAll(encoded, "%20", "+")
    return encoded
}

坑3:大视频的内存问题

如果一次性把整个视频读到内存里,Golang 的优势就没了,一定要用 流式读取,上面代码里用了 os.Open 配合 file.Read,就是基于文件的流式处理,内存只保留当前分片。

性能优化:把速度提上去

并行上传分片

串行上传 500MB 的视频大概要 3 分钟,改成并行上传后(同时上传 3-5 个分片),时间缩短到 1 分钟以内:

func parallelUpload(client *http.Client, uploadURL string, filePath string) error {
    // 使用 buffered channel 控制并发数
    sem := make(chan struct{}, 5) // 最多 5 个并发
    var wg sync.WaitGroup
    for 每个分片 {
        wg.Add(1)
        sem <- struct{}{} // 占位
        go func(chunk []byte, start, end int64) {
            defer wg.Done()
            defer func() { <-sem }()
            // 上传逻辑
        }(chunk, start, end)
    }
    wg.Wait()
}

但要注意:分片必须是有序的,并行上传的分片,服务端会重新排序,所以不用担心乱序问题,不过如果某个分片失败,重试时可能会影响后续分片的顺序,建议加个简单的队列管理。

监控上传进度

给用户反馈是很重要的,可以用回调函数实时更新进度:

type ProgressCallback func(progress float64)
func uploadWithProgress(filePath string, callback ProgressCallback) {
    // 在每次分片上传成功后调用 callback
    // progress = float64(uploadedBytes) / float64(totalBytes)
}

安全性考虑

视频文件往往包含敏感信息,比如培训视频里可能有公司内部数据,上传时建议:

  1. 启用 HTTPS:Golang 的默认 HTTP 客户端支持,无需额外配置
  2. 限制上传权限:给应用分配最小权限,不要用 Site.FullControl
  3. 文件校验:上传后通过 ETag 或 SHA256 验证完整性
func verifyUpload(client *http.Client, siteId, driveId, filePath string) (bool, error) {
    // 获取已上传文件信息
    url := fmt.Sprintf("https://graph.microsoft.com/v1.0/sites/%s/drives/%s/root:/%s",
        siteId, driveId, filePath)
    resp, _ := client.Get(url)
    // 解析响应体,获取 size 和 eTag
    // 比较本地文件大小和远程文件大小是否一致
}

实际部署时的注意事项

环境变量管理

别把客户端密钥硬编码在代码里,用环境变量或者配置文件:

tenantID := os.Getenv("AZURE_TENANT_ID")
clientID := os.Getenv("AZURE_CLIENT_ID")
clientSecret := os.Getenv("AZURE_CLIENT_SECRET")

日志记录

视频上传是异步操作,建议记录关键步骤:

log.Printf("开始上传视频: %s, 大小: %d bytes", fileName, fileSize)
log.Printf("创建上传会话成功,会话ID: %s", sessionID)
log.Printf("分片 %d/%d 上传完成", currentChunk, totalChunks)
log.Printf("视频上传完成,远程路径: %s", remoteURL)

有了日志,排查问题会方便很多,我习惯把日志输出到文件,配合 ELK 或其他日志系统使用。

一个完整的例子

说了这么多,不如给个 demo,假设你要把本地视频 training.mp4 上传到 SharePoint 的 VideoLibrary 文件夹:

func main() {
    client := getTokenClient()
    siteID, _ := getSharePointSite(client, "yourtenant.sharepoint.com:/sites/TeamSite")
    driveID := "b!..." // 可以从 SharePoint API 获取
    uploadSessionURL, _ := createUploadSession(client, siteID, driveID, 
        "VideoLibrary/training.mp4")
    err := uploadChunks(client, uploadSessionURL, "/path/to/training.mp4")
    if err != nil {
        log.Fatalf("上传失败: %v", err)
    }
    fmt.Println("视频上传成功!")
}

这段代码大概能用,但真正生产环境还要加很多错误处理,比如网络波动导致的分片上传失败,就要用指数退避策略重试。

其他办公场景的联想

其实上传视频只是第一步,上传后你还可以做很多事情:

  • 用 Office 365 的 Stream API 生成视频缩略图
  • 调用 Azure Media Services 进行转码
  • 设置 过期时间 自动清理旧视频

这些功能都可以通过 Golang 的 Office 365 SDK 或 REST API 实现,上传视频的流程已经搞定了,后续的功能就是在这个基础上扩展。

写这篇文章的时候,我又重新检查了一遍代码,发现上传视频的那个函数里,Content-Range 的格式我用的是 bytes 0-8388607/2147483648,但官方文档里数字之间没有空格,我赶紧改了一下,顺便加了注释提醒自己,写代码就是这样,总会有一些小疏忽,好在最后跑通了。

希望这篇啰里啰嗦的记录能帮到你,如果你真的用 Golang 往 Office 365 上视频了,记得先检查 token 有没有过期,别问我怎么知道的。

用 Golang 将视频上传到 Office 365,一个开发者的实战手记