使用 .NET 程式存取 SharePoint 文件庫
2 |
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 存取資料的程序比較特殊,不像資料庫直接送查詢拿結果,要依循以下步驟:
- 傳入站台 URL 取得 ClientContext 物件
- 取得 ClientContext.Web 物件
- 呼叫 API 取得清單結果物件(此時物件還沒有內容,可以想成只定義好查詢對象及條件)
- 呼叫 Web.Load(結果物件)
- 呼叫 ClientContext.ExecuteQuery() 連接伺服器執行查詢
- 從結果物件取得查詢結果
補充:有份 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 欄位名稱有點小複雜,需花點時間學習摸索,除了爬文找範例,以下是官方參考資料:
- Collaborative Application Markup Language (CAML) schemas - Microsoft Docs
- SPBuiltInFieldsId Fields - Microsoft Docs
實地測試過用 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;
}
依需求情境,我一共寫了三個常用函式:
- 一次載回整個文件庫所有資料夾及文件資訊,若檔案數較多建議改用第二個 DirDocLibrary
public static SPItemInfo GetDocLibStructure(string siteUrl, string docLibName) - 取回指定資料夾下的子資料夾及檔案清單
public static ListDirDocLibrary(string siteUrl, string docLibName, string folderPath) - 新增或更新指定路徑的文件庫檔案
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 問看看。