SharePoint 文件庫支援 Word/Excel 多人共同線上編輯、版本歷程逭蹤,是共享或協作 Office 文件的好選擇,企業可以選擇自架 SharePoint Portal Server 或是使用 SharePoint Online 或 Office 365 E3 之類的雲端方案。工作及生活上都有文件放在 SharePoint 文件庫共享,前陣子我試著把一些手工活兒自動化,實現用程式查詢文件清單及下載、自動產生文件檔案上傳或更新,這篇文章算是實戰心得整理。

先聲明,我對 SharePoint 研究有限,文章裡的範例是自己摸索出的可行解法,若有人知道更正統或更簡單的寫法,歡迎回饋分享給大家。(這陣子爬文的心得,SharePoint 程式開發相關的中文討論蠻少的,玩的人似乎不多)

要從 .NET Framework 程式存取 SharePoint API,首推 SharePoint CSOM (Client-Site Object Model),它將 SharePoint 清單、項目、檔案等對映成 .NET 物件,取回物件後可使用 LINQ 操作,對 .NET 熟手來說最順手。除此之外,還有 Server OM、JavaScript APIs、REST/OData Endpoints 等其他選擇,適用不同情境,如何選擇可以參考 Choose the right API set in SharePoint

不意外地,SharePoint CSOM 可透過 NuGet 安裝,不用花什麼力氣武器就上膛了:

CSOM 區分 SharePoint Online、SharePoint 2013、SharePoint 2016 等版本,看要連的是 Office 365 或 SPS 2013/2016 決定,但基本作業大部分是相通的,我實測用 Online 版連 SPS 2016 也 OK。

官方文件有一篇 CSOM 基本操作介紹,提供取得清單、建立/刪除項目、讀取欄位、管理群組/角色... 等常用範例。CSOM 存取資料的程序比較特殊,不像資料庫直接送查詢拿結果,要依循以下步驟:

  1. 傳入站台 URL 取得 ClientContext 物件
  2. 取得 ClientContext.Web 物件
  3. 呼叫 API 取得清單結果物件(此時物件還沒有內容,可以想成只定義好查詢對象及條件)
  4. 呼叫 Web.Load(結果物件)
  5. 呼叫 ClientContext.ExecuteQuery() 連接伺服器執行查詢
  6. 從結果物件取得查詢結果

補充:有份 SPS 2010 舊版 CSOM 文件有些額外範例,也值得參考:Common Programming Tasks in the Managed Client Object Model

SharePoint CSOM 有定義一些現成查詢,例如:取回清單所有項目、由清單標題取得清單... 等等,如要執行客製化查詢則需要自己寫 CAML,類似這樣:

var query = new CamlQuery();
//Scope 
// DefaultValue - 清單資料夾加檔案 
// Recursive - 檔案+展開清單資料夾內項目 
// RecursiveAll - 一路查進子資料夾不斷展開
// FilesOnly - 只查檔案
query.ViewXml = $@"<View Scope='RecursiveAll'>
<Query>
    <Where>
        <Eq>
            <FieldRef Name='FileDirRef' />
            <Value Type='Text'>{docLibUrl}{folderPath}</Value>
        </Eq>
    </Where>
</Query>
</View>";
var listItems = docLibList.GetItems(query);

CAML 結構跟 SharePoint 欄位名稱有點小複雜,需花點時間學習摸索,除了爬文找範例,以下是官方參考資料:

實地測試過用 CSOM 讀取 SPS 及 Office 365 文件庫,原則上程式碼大多通用,但有兩個小地方不同,算是眉角吧!

第一個是身分認證方式,公司的 SPS 使用 Windows AD 驗證,若客戶端的登入帳號有設成自動登入 SPS 網站,直接 new ClientContext("https://sps-host.xxx.com/siteName") 即可。Office 365 的話比較麻煩,需要再安裝 SharePointPnPCoreOnline:

程式呼叫 AuthenticationManager.GetWebLoginClientContext(siteUrl) 時會彈出網頁視窗進入 OAuth 登入程序,如此就算有兩階段(雙因子)驗證也不是問題:

程式如要通吃 SPS 跟 Office 365,可以寫成共用函式:

static AuthenticationManager authMan = new AuthenticationManager();
public static ClientContext CreateClientContext(string siteUrl)
{
    //若為 SharePoint Server, 確認使用者可自動登入 siteUrl 並 
    //傳回 new ClientContext(siteUrl) 即可
    if (SharePointServerMode) 
        return new ClientContext(siteUrl);
    return authMan.GetWebLoginClientContext(siteUrl);
}

我遇到的另一個問題是 web.Lists.GetByTitle("清單名稱");。用中文名稱查清單在 SPS 上沒問題,但在 Office 365 會回報找不到清單,查英文名稱 OK,中文名稱不行,懷疑是 GetBytTitle() 有問題,查到一篇文章提到類似問題:

GetByTitle has one aweful shortcoming: it doesn’t work in multilingual environments.

目前我採用的解法是遇到 Office 365 時,改取回所有清單資訊再自己用 LINQ 比對:

public static List GetListByTitle(Web web, string docLibName)
{
    List docLibList;
    var ctx = web.Context;
    if (SharePointServerMode)
    {
        docLibList = web.Lists.GetByTitle(docLibName);
    }
    else //實測 SharePoint Online GetByTitle 找中文名稱有問題,取回清單自己查
    {
        var lists = web.Lists;
        ctx.Load(lists);
        ctx.ExecuteQuery();
        docLibList = lists.Single(o => o.Title == docLibName);
    }
    ctx.Load(docLibList);
    return docLibList;
}

依需求情境,我一共寫了三個常用函式:

  1. 一次載回整個文件庫所有資料夾及文件資訊,若檔案數較多建議改用第二個 DirDocLibrary
    public static SPItemInfo GetDocLibStructure(string siteUrl, string docLibName)
  2. 取回指定資料夾下的子資料夾及檔案清單
    public static List DirDocLibrary(string siteUrl, string docLibName, string folderPath)
  3. 新增或更新指定路徑的文件庫檔案
    public static void InsertOrUpdateFile(string siteUrl, string docLibName, string filePath, byte[] fileContent)

我在 Office 365 的 SharePoint Online 放了簡單的文件庫做測試:

資料夾 A 下有資料夾 X 跟 資料夾 Y,各資料夾零星放了檔案。測試範例做了三個測試 - 列出整個文件庫的目錄結構、列舉「資料夾A」下的子資料夾及檔案、動態產生一個「測試.txt」上傳到「/資料夾C/測試.txt」:

using SharePointTools;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DocListTool
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("** SharePoint 文件庫程式存取展示 **");
                var testNo = Input("請選擇:1) 完整檔案清單  2) 查詢子資料夾 3) 更新文件庫檔案 (1-3): ");
                switch (testNo)
                {
                    case "1":
                        FullList();
                        break;
                    case "2":
                        DirFolder();
                        break;
                    case "3":
                        TestUpload();
                        break;
                    default:
                        Console.WriteLine("無效選擇 - " + testNo);
                        return;
                }
        }

        static string Input(string prompt, bool newLine = false)
        {
            Console.Write(prompt);
            if (newLine) Console.WriteLine();
            Console.ForegroundColor = ConsoleColor.Yellow;
            var res = Console.ReadLine();
            Console.ResetColor();
            return res;
        }

        static void FullList()
        {
            var siteUrl = Input("請輸入站台網址 (例如:https://xxxx.sharepoint.com):", true);
            var docLibName = Input("請輸入文件庫名稱(例如:文件):");
            Console.ForegroundColor = ConsoleColor.Cyan;
            var root = SPDocLibHelper.GetDocLibStructure(siteUrl, docLibName);
            Console.WriteLine($"Directory [{root.Path}]");
            RecursiveDisplay(root.Children, 0);
        }


        static void RecursiveDisplay(IEnumerable<SPItemInfo> items, int level)
        {
            var padding = new string(' ', level * 4);
            foreach (var item in items)
            {
                if (item.FsoType == Microsoft.SharePoint.Client.FileSystemObjectType.Folder)
                {
                    Console.WriteLine($"{padding}[{item.Name}]");
                    if (item.Children.Any()) RecursiveDisplay(item.Children, level + 1);
                }
                else
                    Console.WriteLine($"{padding}{Path.GetFileName(item.Path)}");
            }
        }

        static void DirFolder()
        {
            var siteUrl = Input("請輸入站台網址 (例如:https://xxxx.sharepoint.com):", true);
            var docLibName = Input("請輸入文件庫名稱(例如:文件):");
            var folderPath = Input("請輸入查詢路徑(例如:/資料夾名稱/子資料夾名稱):");
            var items = SPDocLibHelper.DirDocLibrary(siteUrl, docLibName, folderPath);
            Console.ForegroundColor = ConsoleColor.Cyan;
            Console.WriteLine($"資料夾[{folderPath}]下的項目:");
            foreach (var item in items)
            {
                if (item.FsoType == Microsoft.SharePoint.Client.FileSystemObjectType.Folder)
                    Console.WriteLine($"資料夾 <{item.Name}>");
                else
                {
                    Console.WriteLine($"檔案 {Path.GetFileName(item.Path)}");
                    Console.WriteLine(item.Url);
                }
            }
        }

        static void TestUpload()
        {
            var siteUrl = Input("請輸入站台網址 (例如:https://xxxx.sharepoint.com):", true);
            var docLibName = Input("請輸入文件庫名稱(例如:文件):");
            var uploadPath = Input("請輸入查詢路徑(例如:/資料夾名稱/子資料夾名稱):");
            SPDocLibHelper.InsertOrUpdateFile(siteUrl, docLibName, uploadPath, Encoding.UTF8.GetBytes(DateTime.Now.ToString("HH:mm:ss.fff")));
        }
    }
}

測試成功!

完整程式範例有點冗長,我放在 Github,需要的同學請自取。

Example of getting SharePoint document library items and update file with .NET code.


Comments

# by Terence L.

暗黑大大, 您的範例相當實用。 但目前遇到一個問題: docLibName = Documents 用DirFolder() 查詢子資料夾時, 我在 /sites/sitename/Shared Documents/ 下面建立一個 test 資料夾 裡面只有放了 兩個檔案 但是 folderPath 設 /test 回傳卻是 The attempted operation is prohibited because it exceeds the list view threshold 即使在 <Query> 前面加了 <RowLimit>5000</RowLimit> 也是一樣的結果。 folderPath 空白時,會正確回傳有 test 資料夾 請求大大指點明燈

# by Jeffrey

to Terence L. 這方面我研究不深,建議到 https://answers.microsoft.com/zh-hant/msoffice/forum 問看看。

Post a comment