🙌

SharePoint Online の所定のサイトのリストにあるファイルを列挙したい

2024/01/11に公開

PnP Core SDK という OSS のライブラリを使うと Microsoft Graph API と SharePoint の REST API を裏で呼び分けていい感じにしてくれるらしいので、それを使ってやろうと思います。

PnP Core SDK の公式ドキュメントは以下になります。

https://pnp.github.io/pnpcore/

GitHub リポジトリは以下になります。

https://github.com/pnp/pnpcore

MS の中の人がメンテナンスをしているように見えますが MS の製品ではないのでライブラリ自体の不具合については GitHub の Issues などでのサポートになる点については注意が必要です。ただ、裏側は Microsoft Graph API などを叩いているため、そちらで問題が起きている場合は PnP Core SDK とは関係ないので MS のサポートを受けられます。なので、使う場合はどちらの問題なのか切り分けが出来るというのが大事そうですね。

アプリ登録とか下準備

Getting started が良くまとまっています。

https://pnp.github.io/pnpcore/using-the-sdk/readme.html

基本的には Microsoft Entra ID にアプリケーション登録を行い、アクセス先のサイトなども併せて構成をしたあとに、プログラムで実際の処理を行うという流れになります。
私は、今回はログインしたユーザーのかわりに API を叩くのではなくデーモンプロセスから叩くような使い方をしたかったのでアプリケーションの許可でアクセス許可を定義しないといけません。
その時にドキュメント上では以下の強烈な権限を案内されています。

  • SharePoint -> Application Permissions -> Sites -> Sites.FullControl.All
  • SharePoint -> Application Permissions -> TermStore -> TermStore.ReadWrite.All
  • SharePoint -> Application Permissions -> User -> User.ReadWrite.All
  • Microsoft Graph -> Application Permissions -> User -> User.ReadWrite.All
  • Microsoft Graph -> Application Permissions -> Group -> Group.ReadWrite.All

もちろん、やる内容に応じて最低限の権限をつけることをドキュメント上でもお勧めされているので、これをそのまま正直に追加する必要はりません。
今回は列挙したいだけなので Write 系の権限はいらないのと SharePoint のサイトの内容を読むだけなので Sites.Read.All があれば問題ありません。

そして、証明書での認証をするので、以下のドキュメントに従って証明書の準備を行います。

https://learn.microsoft.com/ja-jp/entra/identity-platform/howto-create-self-signed-certificate

とりあえず以下のような PowerShell で Microsoft Entra ID にアップロードする .cer とアプリから使う .pfx を作成します。

$certname = "<<ここに証明書の名前を入れる>>"
$cert = New-SelfSignedCertificate -Subject "CN=$certname" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256
Export-Certificate -Cert $cert -FilePath ".\$certname.cer"
$mypwd = ConvertTo-SecureString -String "<<ここにパスワードを入れる>>" -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath ".\$certname.pfx" -Password $mypwd

.cer は Microsoft Entra ID のアプリ登録にアップロードをしてサムプリントをメモっておきます。

使ってみよう

NuGet から PnP.Core.Auth パッケージを追加したら準備完了です。PnP.Core パッケージもありますが、認証はどうせ必要なので Auth パッケージが必要になります。そして Auth パッケージは Core への依存関係があるので Auth の方だけ入れれば OK です。

PnP Core SDK のいいところは最初から DI サポートをしているところです。ASP.NET Core でもサクッと使えます。
今回は、とりあえず手元で軽く動くコンソールアプリにしたかったのでワーカーサービスにしてみました。

DI コンテナに AddPnPCoreAddPnPCoreAuthentication メソッドでさくっと構成できます。今回はアプリ内にハードコードしていますが、実際に使うときには公式サンプルにある通り appsettings.json や環境変数などから設定を読み込むようにします。

私は設定ファイルや環境変数で試す前に一度ハードコーディングしたお試しプログラムを作って感覚を掴むことをして、その後に設定ファイルなどを使うように本番で組み込むようにするといった流れで確認することが多いです。今回張り付けているのは前者の感覚をつかむための使い捨てのコードです。

Program.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var app = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddPnPCore(options =>
        {
            // 色々設定できる
            options.DisableTelemetry = true;
            options.HttpRequests = new()
            {
                UserAgent = "PnPLab",
                SharePointRest = new()
                {
                    UseRetryAfterHeader = true,
                },
            };

            // アプリで使用するサイトを登録する。
            options.Sites.Add("Retail", new()
            {
                SiteUrl = "https://<<hogehoge>>.sharepoint.com/sites/<<SiteName>>",
            });

            options.PnPContext = new()
            {
                GraphAlwaysUseBeta = false,
                GraphCanUseBeta = false,
                GraphFirst = true,
            };
        });

        services.AddPnPCoreAuthentication(options =>
        {
            // 認証の設定
            options.Sites.Add("Retail", new()
            {
                AuthenticationProviderName = "Default",
            });

            options.Credentials.DefaultConfiguration = "Default";
            options.Credentials.Configurations.Add("Default", new()
            {
                // Microsoft Entra ID に登録したアプリケーションの情報を設定する
                TenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
                ClientId = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
                X509Certificate = new()
                {
                    Thumbprint = "<<Microsoft Entra ID にアップロードした証明書のサムプリント>>",
                    // ↓はファイルから読み込む場合の例
                    Certificate = new(".\\証明書のファイル名.pfx", "証明書のパスワード"),
                    // 実際にはストアから読み込むようにするので以下のように書くことのほうが多いと思う
                    // StoreName = StoreName.My,
                    // StoreLocation = StoreLocation.CurrentUser,
                }
            });
        });

        // Worker で実際の処理をする
        services.AddHostedService<Worker>();
    })
    .UseConsoleLifetime()
    .Build(); ;

app.Run();

このように下準備さえしてしまえば、ワーカーでこんな感じで使えます。

Worker.cs
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using PnP.Core.Model.SharePoint;
using PnP.Core.QueryModel;
using PnP.Core.Services;

internal class Worker(
    IPnPContextFactory pnpContextFactory, 
    ILogger<Worker> logger, 
    IHostApplicationLifetime applicationLifetime) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Program.cs で Retail という名前で構成したサイトに繋ぐためのコンテキストを作成
        using var context = await pnpContextFactory.CreateAsync("Retail");
        // Documents というタイトルのリストのルートフォルダーを取得
        var folder = (await context.Web.Lists.GetByTitleAsync("Documents", x => x.RootFolder)).RootFolder;

        // フォルダー内のファイルを再帰的に取得
        var files = new List<string>();
        await CollectFilesAsync(folder, files);

        // ファイル名を出力
        logger.LogInformation("Files: {files}", string.Join("\n", files));

        applicationLifetime.StopApplication();
    }

    private async Task CollectFilesAsync(IFolder folder, List<string> files)
    {
        // フォルダー内のファイルを取得
        await foreach (var file in folder.Files.AsAsyncEnumerable())
        {
            files.Add(file.Name);
            try
            {
                // こんな感じでファイルのダウンロードもできる。
                // 一部、もともと SharePoint Online にある隠しフォルダーの中身のファイルとかは Sites.Read.All ではダウンロードできなかった。
                await File.WriteAllBytesAsync(file.Name, await file.GetContentBytesAsync());
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Error");
            }
        }

        // サブフォルダーを再帰的に取得
        await foreach (var subFolder in folder.Folders.AsAsyncEnumerable())
        {
            await CollectFilesAsync(subFolder, files);
        }
    }
}

実行するとこんな感じでファイル名が出力されます。

info: Worker[0]
      Files: Electronics Store Trends in International Markets.pptx
Eastern Region Retail Performance.docx
Contoso Electronics to Open Miami Store.docx
New In-Store Customer Service Counters.pptx
Cost and Pricing Analysis - Western Region.xlsx
NC460 Line Feature Comparison.docx
NC460 Sales Team.pbix
Sales Results Overview.xlsx
Sales Process.vsdx
Org Chart.vsdx
ドキュメント.docx
repair.aspx
template.dotx
AllItems.aspx
DispForm.aspx
Upload.aspx
Combine.aspx
EditForm.aspx
Thumbnails.aspx
Global Sales Performance.pbix
Contoso Electronics Outdoor Sale Flyer.docx
CE Annual Report.docx
Contoso NextGen Camera Product Planning.docx
P and L Summary.xlsx
Contoso Electronics Outdoor Sale Brochure.docx
Ad Slogans.docx

aspx とかが落ちてくるのが邪魔ですね…。うまく弾けないかなぁ。

まとめ

PnP Core SDK 結構いけてていい感じ。思ったよりモダンだった。

Microsoft (有志)

Discussion