从 SharePoint 获取文件及其历史版本(基于 GoSIP SDK)

Published: 2024-02-01

Tags: Golang

本文总阅读量

本文记录了一些 GoSIP 获取 SharePoint 的示例,相对来说重点是获取历史版本,虽说 GoSIP 尽量做到了开箱即用,但是对开发者来说,本身对 SharePoint 服务也需要有所了解,并熟悉其 REST API 风格。

如果你需要通过 Golang 程序读写 SharePoint 上的文件,相信本篇笔记会对你有所帮助。

环境说明

SharePoint Root Site

在「文档」类别的根目录下有一个 test.txt 文件

SharePoint 子网站

在 Root Site 的「网站内容」类别下,点击「新建 - 子网站」进行创建

标题为「文档中心」,路径为 subtest

同样,创建一个测试文件,名为 subsite_test.txt

获取文件的路径

勾选文件,在右侧面板可以找到文件的路径

得到文件的路径

https://mycompany.sharepoint.com/sites/appsite/Shared%20Documents/test.txt

SubSite 同理,获取 subsite_test.txt 的路径为

https://mycompany.sharepoint.com/sites/appsite/subsite/Documents/subsite_test.txt

这里可以看到一些区别,根站点的「文档」应为对应 “Shared Documents”,子站点则为 “Documents”

Golang GoSIP 库简介

GoSIP 地址:GoSIP - SharePoint SDK for Go

微软的 REST API 很难用现代化的接口调用方式去理解,看 SharePoint 分散在不同地方的文档糟心的很,还好有 GoSIP 库,它对 SharePoint API 做了封装,使用体验良好。

官方文档清晰易懂,以下众多示例也是 GoSIP 使用的 Examples,GoSIP 支持多种认证方式,以下示例均使用的用户名/密码认证方式进行的验证(saml)

代码示例(1)用户名密码认证

认证并获取网站标题

auth.go

package main

import (
    "fmt"
    "github.com/koltyakov/gosip"
    "github.com/koltyakov/gosip/api"
    strategy "github.com/koltyakov/gosip/auth/saml"
    "log"
)

func main() {

    authCfg := &strategy.AuthCnfg{
        SiteURL:  "https://mycompany.sharepoint.com/sites/appsite",
        Username: "<user-user-email>",
        Password: "<user-user-password>",
    }

    client := &gosip.SPClient{AuthCnfg: authCfg}

    sp := api.NewSP(client)

    resp, err := sp.Web().Get()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("网站标题:%s\n", resp.Data().Title)
}

输出

AppSite

示例代码(2)获取文件信息

package main

import (
    "fmt"
    "github.com/koltyakov/gosip"
    "github.com/koltyakov/gosip/api"
    strategy "github.com/koltyakov/gosip/auth/saml"
    "log"
)

func main() {

    authCfg := &strategy.AuthCnfg{
        SiteURL:  "https://mycompany.sharepoint.com/sites/appsite",
        Username: "<user-user-email>",
        Password: "<user-user-password>",
    }

    client := &gosip.SPClient{AuthCnfg: authCfg}

    sp := api.NewSP(client)

    fileURL := "Shared Documents/test.txt"

    // 获取文件信息
    fileResp, err := sp.Web().GetFileByPath(fileURL).Get()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("FileName: %v\n", fileResp.Data().Name)
    fmt.Printf("FileSize: %d\n", fileResp.Data().Length)
    fmt.Printf("UniqueID: %s\n", fileResp.Data().UniqueID)
    fmt.Printf("MajorVersion: %d\n", fileResp.Data().MajorVersion)
    fmt.Printf("MinorVersion: %d\n", fileResp.Data().MinorVersion)
    fmt.Printf("ServerRelativeURL: %s\n", fileResp.Data().ServerRelativeURL)
    fmt.Printf("TimeCreated: %s\n", fileResp.Data().TimeCreated)
    fmt.Printf("TimeLastModified: %s\n", fileResp.Data().TimeLastModified)
}

输出

FileName: test.txt
FileSize: 15
UniqueID: da16b19f-ff22-4646-9ed2-818dda806d9f
MajorVersion: 3
MinorVersion: 0
ServerRelativeURL: /sites/appsite/Shared Documents/test.txt
TimeCreated: 2024-01-24 08:19:07 +0000 UTC
TimeLastModified: 2024-01-30 07:55:06 +0000 UTC

从输出的结果可以看到,传入的路径是 Shared Documents/test.txt,文件的 ServerRelativeURL 是 /sites/appsite/Shared Documents/test.txt,SDK 对请求时的路径做了自动补全

另外对于子站点的文件请求,将 fileURL 变量修改为如下地址,子站点的请求均需携带。

fileURL := "subsite/Documents/subsite_test.txt"

示例代码(3)下载文件

package main

import (
    "github.com/koltyakov/gosip"
    "github.com/koltyakov/gosip/api"
    "log"
    "os"

    strategy "github.com/koltyakov/gosip/auth/saml"
)

func main() {

    authCfg := &strategy.AuthCnfg{
        SiteURL:  "https://mycompany.sharepoint.com/sites/appsite",
        Username: "<user-user-email>",
        Password: "<user-user-password>",
    }

    client := &gosip.SPClient{AuthCnfg: authCfg}

    sp := api.NewSP(client)

    fileURL := "Shared Documents/test.txt"

    data, err := sp.Web().GetFile(fileURL).Download()
    if err != nil {
        log.Fatal(err)
    }

    file, err := os.Create("./test.txt")
    if err != nil {
        log.Fatalf("unable to create a file: %v\n", err)
    }
    defer file.Close()

    _, err = file.Write(data)
    if err != nil {
        log.Fatalf("unable to write to file: %v\n", err)
    }
    file.Sync()
}

示例代码(4)下载文件的历史版本

文件每次编辑,都会产生历史版本

download-version.go

package main

import (
    "fmt"
    "github.com/koltyakov/gosip"
    "github.com/koltyakov/gosip/api"
    strategy "github.com/koltyakov/gosip/auth/saml"
    "log"
)

func majorVersionToVersionId(major int) int {
    return 512 * major
}

func main() {

    authCfg := &strategy.AuthCnfg{
        SiteURL:  "https://mycompany.sharepoint.com/sites/appsite",
        Username: "<user-user-email>",
        Password: "<user-user-password>",
    }

    client := &gosip.SPClient{AuthCnfg: authCfg}

    spClient := api.NewHTTPClient(client)

    fileURL := "test.txt"
    versionId := majorVersionToVersionId(2)
    endpoint := fmt.Sprintf("%s/_api/web/GetFileByServerRelativeUrl('/sites/appsite/Shared Documents/%s')/Versions/GetById(%d)/$value", authCfg.GetSiteURL(), fileURL, versionId)

    data, err := spClient.Get(endpoint, nil)
    if err != nil {
        log.Fatal()
    }
    fmt.Println(string(data))
}

这里是用的是 GoSIP 提供的更加底层的 HTTPClient,请求自定义接口路径,如果想要用文件 ID 定位问文件,可以修改代码片段

fileId := "da16b19f-ff22-4646-9ed2-818dda806d9f"
versionId := majorVersionToVersionId(2)
endpoint := fmt.Sprintf("%s/_api/web/GetFileById('%s')/Versions/GetById(%d)/$value", authCfg.GetSiteURL(), fileId, versionId)

另外 REST API 也支持通过原始路径请求历史文件,上示例的 endpoint 替换即可

endpoint = fmt.Sprintf("%s/_vti_history/%d/Shared Documents/test.txt", authCfg.GetSiteURL(), versionId)

总结一下,就是以下 API 地址从获取历史文件内容来看都是等价的:

# 通过相对路径
https://mycompany.sharepoint.com/sites/appsite/_api/web/GetFileByServerRelativeUrl('/sites/appsite/Shared Documents/test.txt')/Versions/GetById(1024)/$value

# 通过文件ID
https://mycompany.sharepoint.com/sites/appsite/_api/web/GetFileById('da16b19f-ff22-4646-9ed2-818dda806d9f')/Versions/GetById(1024)/$value

# 通过历史版本位置
https://mycompany.sharepoint.com/sites/appsite/_vti_history/1024/Shared Documents/test.txt

示例代码(5)获取文件及文件夹列表

package main

import (
    "fmt"
    "github.com/koltyakov/gosip"
    "github.com/koltyakov/gosip/api"
    strategy "github.com/koltyakov/gosip/auth/saml"
    "log"
)

func main() {

    authCfg := &strategy.AuthCnfg{
        SiteURL:  "https://mycompany.sharepoint.com/sites/appsite",
        Username: "<user-user-email>",
        Password: "<user-user-password>",
    }
    client := &gosip.SPClient{AuthCnfg: authCfg}

    sp := api.NewSP(client)

    // 获取并遍历所有文件
    folderURL := "Shared Documents/"
    items, err := sp.Web().GetFolder(folderURL).Files().Get()
    if err != nil {
        log.Fatalf("无法获取文件: %v\n", err)
    }
    for _, file := range items.Data() {
        fmt.Printf(file.Data().Name)
    }

    // 获取并遍历所有子文件夹
    folders, err := sp.Web().GetFolder(folderURL).Folders().Get()
    if err != nil {
        log.Fatalf("无法获取子文件夹: %v\n", err)
    }
    for _, folder := range folders.Data() {
        fmt.Printf(folder.Data().Name)
    }
}

版本号规则的补充

文件既可以有用整数(12.0)表示的主要版本(Major Version),也可以有用十进制数(12.3)表示的次要版本(Minor Version)。

如果将库配置为支持「签入/签出」,则用户对签出文档执行的每次更改都将创建次要版本。

如果直接在线编辑文件,那么将创建主要版本。

获取历史文件时的文件 ID 计算公式:VersionID = MajorVersion * 512 + MinorVersion

不建议使用 SharePoint 的 SubSite

推荐使用 Hub Site 替代 SubSite 的组织方式,因为 Hub Site 组织更加的清晰,易于扩展。

  1. Why use SharePoint Hub Sites and not SharePoint Sub Sites!
  2. SharePoint Online --中心站点介绍(hub site)

这不是本文的重点,故不再发散介绍。

SharePoint 请求 Proxy

推荐这个库:https://www.npmjs.com/package/sp-rest-proxy

如果需要适配较多的 SharePoint 原始 API,建议使用这个工具提供的页面便于调试,前端开发的时候也会更加的便捷。

其它补充

自己的网页嵌入 SharePoint 的文件地址,txt 文件能够展示,xlsx 则不展示

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>SharePoint XLSX 文件嵌入示例</title>
</head>
<body>
  <h1>SharePoint XLSX 文件嵌入示例</h1>
  <iframe src="https://mycompany.sharepoint.com/sites/appsite/_layouts/15/embed.aspx?UniqueId=d09c1141-5111-4a00-97de-1cc6c16c501b" width="640" height="360" frameborder="0" scrolling="no" allowfullscreen title="test-xlsx.xlsx"></iframe>
  <hr/>
  <br/>
  <iframe src="https://mycompany.sharepoint.com/sites/appsite/_layouts/15/embed.aspx?UniqueId=da16b19f-ff22-4646-9ed2-818dda806d9f" width="640" height="360" frameborder="0" scrolling="no" allowfullscreen title="test.txt"></iframe>
</body>
</html>

效果

看控制台报错是因为找不到 “https://res-1.cdn.office.net/files/odsp-web-prod_2024-01-19.010/monaco-worker.js” 这个文件,看起来是编辑器没有加载完全的原因,这倒不是重点,先这样。

参考