SPOライブラリのフォルダをZip化してダウンロードする!(無償範囲で)※外人MVP様のフローをちょっと改良

はじめに

この記事は、Power Automate Advent Calendar 2023 12月12日 シリーズ2担当分の記事です。
(さかのぼり登録させていただきました!)
Power Apps Advent Calendar 2023 にも投稿しています。こちらもぜひ!

今回はPower AutomateでSPOからフォルダをZip化してダウンロードする実装の記事です。
2年ほど前に外国のMVPさんの記事(Paul ポールさん)を見て、「おー!こんなん出来るんだ!すげー!」と当時にまねて作成。

ただし件数の問題などがあり、調整してある程度いい感じにはなったんですが、やっていることがトリッキーなので、日の目を浴びることがなかった(というか実運用での利用はやめといた)んですが、限定的でも使いたいっていう方もいるかなーと思い今回記事にしてみました。

NOTE本実装については利用が限定的である点、SPOが裏でやっている処理をREST APIで実装している感じなので、本格的なシステムの実運用に取りこむなどは推奨しません。あくまで出来る範囲でもうれしいからやってみたいという方向けになります。
自分も細かい部分まで把握できていないのでうまくいかない場合などのサポートは難しいのでその辺はご了承ください。

サンプル動画

ひさしぶりに動画も取ってみました。
SPOライブラリのフォルダに複数ファイルがあり、それをアプリからフローを呼び出してZipファイル化、
そのパスをアプリへ返却してダウンロードの流れです。

Zip化するアクションについて

2023年12月時点、Power Apps、Power AutomateではZip化するアクションは
標準やプレミアムライセンスの範囲でも存在しません。(そのはず、間違っていたらすいません)
ただしサードパティのコネクタを使用すれば出来ます。この場合はサードパティ側のライセンスが必要となります。

以下がよく見る代表的なものかと思います。こちらを使用すればZipファイル作成も出来るようですので、
本格的なシステムで必要であればご検討いただくのが良いかと思います。
新しい Power Automate アクション: アーカイブに追加 (ZIP) |エンコディアン (encodian.com)

今回紹介する実装はポールさんがスタンドアロンライセンスが不要な範囲でSPOを使って
それを実現させたという内容になります。

本家の記事

本家の記事は以下です。

こちらのとおり実装すれば基本SPOライブラリの指定フォルダをZipファイルに変換できます。
Power Appsから呼んでいる場合はそのPathを返却してDownLoad関数でダウンロードも可能です。

実装の画面ショットと定義コードをそのまま載せてくれています。
要はSPOだと標準で複数ファイルを選択してダウンロードするとZipにしてくれる機能を
SPOのREST APIを使って実現するというイメージです。

ただ、上記のままの場合は2点ほど問題がありました。
・30件以上が取れない(投稿のコメントにもあり)
・ある程度ファイル数が多いとエラーとなる(uriComponentで文字数超過のエラーが出る)

上記2点については自分のほうで実装を調整してみたところ、100件以上でも取れるのを確認しました。

ただ、いずれにしろ容量が100MB以上となるとダウンロードする時点でエラーになります。
(フローの制限のため回避できず)
ですので、30件以上やある程度の件数は取れるようにはできましたが限界は100MBまでとなります。

TIPS※本サンプルでは作成時のチャンクの許可をオフにして上書き更新を出来るようにしてます。
オフにした場合は作成時の上限は90MBまでなのでその場合は90MBが上限と言えます
※チャンクの許可や作成や取得の容量上限については以下記事をご参照ください。
SPOファイル作成時 チャンクの許可をオフ の対応容量について | Power Apps Tips ログ (youseibubu.com)

NOTE作成するファイルはZipファイルではありますが圧縮はされていません。
なのでそのままの容量となります(これはSPO標準でも同じだった気と思います)
そのため容量を抑える目的では使えないとなります。フォルダごとダウンロードしたい用途向けかと
※ポールさんの場合、元々はワードファイルを編集するためにZipにしたいという目的だったようです

実装(修正後)

以下にポールさんのフローを一部自分が修正したフローについて記載します。
※サンプル用にシンプルにしています。実際のユースケースに合わせて調整ください。

以下のサンプルでは作成したZipファイルのパスをアプリへ返却してパスからZipファイルをダウンロードできるようにしています。
※作成したZipファイル自体を返却してもダウンロードは不可なのでパスを渡す。
※Zipファイルはしばらくして削除するなど工夫する

全体像

全体像

変数定義

サンプルでは以下のように定義しています。ご自身のものに置き換えていただくイメージです。

TIPS対象フォルダなどはサンプルでは固定値で指定していますが、本来はアプリ側でフォルダパスを指定したり、処理の中でダウンロードしたい複数のファイルを作成してフォルダに詰めてそのパスを指定する。など動的なものとなるかと思います。そのあたりはユースケースに置き換えて実装ください。

  • SiteURL →サイトのURLを指定
  • ExportLibraryPath →ライブラリのPath部分を指定(半角スペースはエンコード不要)
  • ExportFolderURL  →ライブラリ配下で対象とするフォルダのパス部分(複数階層の場合は/で区切る)
  • ZipFolder     →作成するZipファイルを配置するフォルダ ※任意の場所

上記の場合は以下SPOサイトのライブラリ(ドキュメント)は以下のフォルダ(DOC1/test)を指定となります。
https://○○〇.sharepoint.com/sites/HP/Shared%20Documents/DOC1/test

RenderListDataAsStream で必要な情報を取得 ★調整ポイント

この部分はポールさんが言っているとおり、SharePointのREST APIを使ってRenderListDataAsStermというやつでアクセストークンやサイズ、メディアのURLなどの情報を取ってきています。
いきなり裏技な感じでややこしいですね!この情報を後の作成時に使用します。

SharePointRESTAPIのリファレンスの記事
RESTを使用したリストとリスト項目の操作 |Microsoft Learn

SharePoint REST API の部分

まず、REST API内で使用するライブラリのパス(サイトとライブラリ)とZip化するフォルダのパスをSettingsで定義しています。ここは変数を使って組み立ててます。

CodeSettings:
{
“libraryPath”: “@{variables(‘SiteURL’)}/@{variables(‘ExportLibraryPath’)}”,
“zipFolderPath”: “/@{variables(‘ExportFolderURL’)}”
}

そしてREST APIではPOSTにして、URIは上記のように書きます。これは元の記事そのままです。動的な部分をSettingの項目から取ってUriエンコードしている感じです。

CodeURI:
_api/web/GetListUsingPath(DecodedUrl=@a1)/RenderListDataAsStream?@a1=%27@{encodeUriComponent(outputs(‘Settings’)[‘libraryPath’])}%27&RootFolder=@{encodeUriComponent(concat(outputs(‘Settings’)[‘libraryPath’], outputs(‘settings’)[‘zipFolderPath’]))}

★このボディの部分を調整しています。以下のように調整します。

Codeボディ:
{“parameters”: {“RenderOptions”: 4103,”ViewXml”: “<View><RowLimit >1000</RowLimit></View>”}}

TIPSポールさんの記事では{“RenderOptions”: 4103}}ですが、そこにViewXmlでRowLimit1000を追加しています。
これで1000行まで取れます(実際にはそこまで行くと別の箇所でエラーとなると思いますが)
※この辺は公式のページでパラメータなどみて試しながらやった記憶です

上記の箇所を元々の部分から調整して1000行まで増やしています。これで30件しか取れない問題は解消しました。

次にアクセストークンを作成アクションに保持しています。

Code作成アクション:accessToken
@{outputs(‘SharePointHTTP’)?[‘body’][‘ListSchema’][‘.driveAccessToken’]}

取得した情報を選択アクションで調整 ★調整ポイント

次にその他取得したレコード(アイテム)の各情報を選択アクションで成形しています。

★ここを調整しています。
ポールさんの記事だと選択アクションでそのまま項目を指定して、downloadZipのアクションでそれをuriComponentでエンコードしているんですが、その場合に文字数がある程度多いとエラーとなりました。
(上記の対応で30件を突破したけどもう少し増やしたらこのエラーが出た感じ)

そのため、この選択アクション部分で個別にエンコードする実装にしています。
合わせてJSONの部分は別でリプレイスして対応しています。この対応でこの部分のエラーは出なくなりました。

以下がもともとのポールさんのものです。

ポールさんの実装 downloadZipのボディの中でエンコードしている

ここを以下のように調整しています。nameとdocIdをそれぞれエンコード
※docIdには事前取得したアクセストークンを含めている

Code開始:@{body(‘SharePointHTTP’)[‘ListData’][‘Row’]}
マップ:
name :  uriComponent(item()[‘FileLeafRef’])
size : item()[‘SMTotalSize’]
docId :  uriComponent(concat(item()[‘.spItemUrl’],’&’,outputs(‘accessToken’)))
isFolder :  if(equals(item()[‘FSObjType’],’1′), true, false)

Attachment Items(作成アクション)の部分はそのまま
Attachment Items EncodeUri(作成アクション)を追加して{、}なんかをリプレイスでエンコードしています。
※あんまり覚えていないのですが上記の個別エンコードの対応だけだとJSON部分のエンコードが出来ないのでこれでやった記憶です💦もっといい方法があるかもしれない。

CodeAttachment Items:
{
“items”: @{body(‘Select’)}
}

CodeAttachment Items EncodeUri:
@{replace(replace(replace(replace(replace(replace(replace(string(outputs(‘Attachment_Items’)),'{‘,’%7B’),'”‘,’%22′),’:’,’%3A’),'[‘,’%5B’),’,’,’%2C’),’}’,’%7D’),’]’,’%5D’)}

Zipファイル情報を取得し作成する

以下の部分で実際にZipファイルにする情報をダウンロードしてそれをコンテンツにしてファイル作成しています。
調整した箇所としては、ボディの部分を先ほどのエンコードした内容にしたのと、
ファイル作成部分を変数で指定した場所に作成するようにしたくらいです。

またこのサンプルではSPO上にそのままZipを作成してそのパスをアプリへ返却しています。
ポールさんの記事だとOneDriveに保存するパターンも載っています。

NOTEdownloadZipの内容は正直詳細までわかってませんが、ポールさんの通り実装していて、
こうやるとうまいことダウンロードできるんだね!という感じです。

CodedownloadZip:
サイトのアドレス:@body(‘SharePointHTTP’)[‘ListSchema’][‘.mediaBaseUrl’]方法:POST
URI:/transform/zip?cs=@{body(‘SharePointHTTP’)[‘ListSchema’][‘.callerStack’]}
ヘッダー:
 Content-Type : application/x-www-form-urlencoded
ボディ:
zipFileName=test.zip&guid=@{guid()}&provider=spo&files=@{outputs(‘Attachment_Items_EncodeUri’)}&oAuthToken=

CodeZipファイル作成:
サイトのアドレス:@variables(‘SiteURL’)
フォルダーのパス:@{variables(‘ExportLibraryPath’)}/@{variables(‘ZipFolder’)}
ファイル名:@{formatDateTime(addHours(utcNow(),9),’yyyyMMdd’)}.zip
ファイルコンテンツ:@{base64ToBinary(body(‘downloadZip’)[‘$content’])}

Codeアプリへ応答:
path: @{variables(‘SiteURL’)}@{outputs(‘Zipファイル作成’)?[‘body/Path’]}

サンプル動作

では実際に以下フォルダにあるファイルをZip化してダウンロードしてみます。
このフォルダには91個のファイル(テキスト、画像やエクセルなど)を置いています。
フローだけで動かしてもZipファイルは出来ますが、今回はアプリから呼び出してZipをそのままダウンロードします。

アプリから呼び出してZipをダウンロード

アプリから呼び出すサンプルとして以下のアプリを作成しました。
ボタンでフローを呼び出し、戻り値(Path)をそのままダウンロードします。
※下のラベルにはPathを表示

TIPSDownload関数でZipファイルのパスを指定すればそのままダウンロード画面が開く動作となります。
※ダウンロードしたファイル自体を渡してもダウンロードはしてくれませんので注意


ボタンをクリックしてフロー実行します。今回の量だと20秒ほど待てば返ってきました。
そのままダウンロード画面が開き、ダウンロードが出来ます。
ファイルの中身を開くとちゃんと91ファイル取れています。

ファイルもちゃんとしている

フローの結果

フローの結果をみると以下のようにdownloadZipの部分である程度時間がかかっています。
容量やサイズによって変わってきますね。

今回は91ファイルで容量は27.2MBでした。この場合でトータルで20秒位でした。
→60ファイル位、容量は全体で1MBほどの場合は4、5秒で返ってきました。特に容量に起因します。

Zipファイルは後で消すなど調整

フローの処理でZipファイルを作成していて、そのパスをアプリへ渡しています。
アプリへZipファイルそのものを渡してもダウンロードしてくれないので一旦SPOやOneDrive上に作成する必要があります。
そのためダウンロードしてもらうまではこのファイルは必要で、その後に削除する必要があります(残しておいてもいいが基本不要)

SPOライブラリの指定の場所にZipファイルが出来ている

今回のサンプルではそのままにしていますが、一定時間待機した後にフロー内でファイルを削除したり、定期的に動作させる別のスケジュールフローで削除するなどで対応するのが良いかと思います。

実装コード

今回実装した内容のコードのスコープで囲った部分を一応貼っておきます。
(定義は順不同なのでバラバラの順番です。変数の部分は貼っていないので記事を参考ください)

以下を開くと展開されます

実装コード:スコープ部分

Code{
“id”: “b0ffd77a-7ecd-43ba-8934-b3f9dcb87566”,
“brandColor”: “#8C3900”,
“connectionReferences”: {
“shared_sharepointonline_1”: {
“connection”: {
“id”: “/providers/Microsoft.PowerApps/apis/shared_sharepointonline/connections/d72b6dcc08944762b56ad1d59d9f6c78”
}
},
“shared_sharepointonline”: {
“connection”: {
“id”: “/providers/Microsoft.PowerApps/apis/shared_sharepointonline/connections/d72b6dcc08944762b56ad1d59d9f6c78”
}
}
},
“connectorDisplayName”: “制御”,
“icon”: “data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDMyIDMyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KIDxwYXRoIGQ9Im0wIDBoMzJ2MzJoLTMyeiIgZmlsbD0iIzhDMzkwMCIvPg0KIDxwYXRoIGQ9Im04IDEwaDE2djEyaC0xNnptMTUgMTF2LTEwaC0xNHYxMHptLTItOHY2aC0xMHYtNnptLTEgNXYtNGgtOHY0eiIgZmlsbD0iI2ZmZiIvPg0KPC9zdmc+DQo=”,
“isTrigger”: false,
“operationName”: “スコープ_Zipファイル化”,
“operationDefinition”: {
“type”: “Scope”,
“actions”: {
“Settings”: {
“type”: “Compose”,
“inputs”: {
“libraryPath”: “@{variables(‘SiteURL’)}/@{variables(‘ExportLibraryPath’)}”,
“zipFolderPath”: “/@{variables(‘ExportFolderURL’)}”
},
“runAfter”: {},
“metadata”: {
“operationMetadataId”: “19be0900-ca0f-462d-bc96-b35875054239”
}
},
“SharePointHTTP”: {
“type”: “OpenApiConnection”,
“inputs”: {
“host”: {
“connectionName”: “shared_sharepointonline”,
“operationId”: “HttpRequest”,
“apiId”: “/providers/Microsoft.PowerApps/apis/shared_sharepointonline”
},
“parameters”: {
“dataset”: “@variables(‘SiteURL’)”,
“parameters/method”: “POST”,
“parameters/uri”: “_api/web/GetListUsingPath(DecodedUrl=@a1)/RenderListDataAsStream?@a1=%27@{encodeUriComponent(outputs(‘Settings’)[‘libraryPath’])}%27&RootFolder=@{encodeUriComponent(concat(outputs(‘Settings’)[‘libraryPath’], outputs(‘settings’)[‘zipFolderPath’]))}”,
“parameters/body”: “{\”parameters\”: {\”RenderOptions\”: 4103,\”ViewXml\”: \”<View><RowLimit >1000</RowLimit></View>\”}}”
},
“authentication”: “@parameters(‘$authentication’)”
},
“runAfter”: {
“Settings”: [
“Succeeded”
]},
“metadata”: {
“operationMetadataId”: “9f277fb1-0da1-444a-825c-af20460a30f9”
}
},
“accessToken”: {
“type”: “Compose”,
“inputs”: “@outputs(‘SharePointHTTP’)?[‘body’][‘ListSchema’][‘.driveAccessToken’]”,
“runAfter”: {
“SharePointHTTP”: [
“Succeeded”
]},
“metadata”: {
“operationMetadataId”: “bdd4579b-444c-467c-a3bc-08bd225c5c9f”
}
},
“Select”: {
“type”: “Select”,
“inputs”: {
“from”: “@body(‘SharePointHTTP’)[‘ListData’][‘Row’]”,
“select”: {
“name”: “@uriComponent(item()[‘FileLeafRef’])”,
“size”: “@item()[‘SMTotalSize’]”,
“docId”: “@uriComponent(concat(item()[‘.spItemUrl’],’&’,outputs(‘accessToken’)))”,
“isFolder”: “@if(equals(item()[‘FSObjType’],’1′), true, false)”
}
},
“runAfter”: {
“accessToken”: [
“Succeeded”
]},
“metadata”: {
“operationMetadataId”: “a62aaf0c-ccbc-4ef1-b16e-61442bdabcfa”
}
},
“Attachment_Items”: {
“type”: “Compose”,
“inputs”: {
“items”: “@body(‘Select’)”
},
“runAfter”: {
“Select”: [
“Succeeded”
]},
“metadata”: {
“operationMetadataId”: “48822e82-90f6-4ffb-853b-51632d994a8c”
}
},
“downloadZip”: {
“type”: “OpenApiConnection”,
“inputs”: {
“host”: {
“connectionName”: “shared_sharepointonline_1”,
“operationId”: “HttpRequest”,
“apiId”: “/providers/Microsoft.PowerApps/apis/shared_sharepointonline”
},
“parameters”: {
“dataset”: “@body(‘SharePointHTTP’)[‘ListSchema’][‘.mediaBaseUrl’]”,
“parameters/method”: “POST”,
“parameters/uri”: “/transform/zip?cs=@{body(‘SharePointHTTP’)[‘ListSchema’][‘.callerStack’]}”,
“parameters/headers”: {
“Content-Type”: “application/x-www-form-urlencoded”
},
“parameters/body”: “zipFileName=test.zip&guid=@{guid()}&provider=spo&files=@{outputs(‘Attachment_Items_EncodeUri’)}&oAuthToken=”
},
“authentication”: “@parameters(‘$authentication’)”
},
“runAfter”: {
“Attachment_Items_EncodeUri”: [
“Succeeded”
]},
“metadata”: {
“operationMetadataId”: “92830437-711d-42fd-b00c-1678aefdc9f1”
}
},
“Zipファイル作成”: {
“type”: “OpenApiConnection”,
“inputs”: {
“host”: {
“connectionName”: “shared_sharepointonline_1”,
“operationId”: “CreateFile”,
“apiId”: “/providers/Microsoft.PowerApps/apis/shared_sharepointonline”
},
“parameters”: {
“dataset”: “@variables(‘SiteURL’)”,
“folderPath”: “@{variables(‘ExportLibraryPath’)}/@{variables(‘ZipFolder’)}”,
“name”: “@{formatDateTime(addHours(utcNow(),9),’yyyyMMdd’)}.zip”,
“body”: “@base64ToBinary(body(‘downloadZip’)[‘$content’])”
},
“authentication”: “@parameters(‘$authentication’)”
},
“runAfter”: {
“downloadZip”: [
“Succeeded”
]},
“metadata”: {
“operationMetadataId”: “e4e57526-c8a0-4807-b381-db0c3f573457”
}
},
“Attachment_Items_EncodeUri”: {
“type”: “Compose”,
“inputs”: “@replace(replace(replace(replace(replace(replace(replace(string(outputs(‘Attachment_Items’)),'{‘,’%7B’),’\”‘,’%22′),’:’,’%3A’),'[‘,’%5B’),’,’,’%2C’),’}’,’%7D’),’]’,’%5D’)”,
“runAfter”: {
“Attachment_Items”: [
“Succeeded”
]},
“description”: “件数が多い場合に全体にUr多い場合に全体にuriComponentをかけるとエラーとなるケースがあるため、個別にuriComponentを行い、JSON書式部分はReplaceで対応”,
“metadata”: {
“operationMetadataId”: “619edddc-5e84-4af6-a091-2d9abc04c7ec”
}
}
},
“runAfter”: {
“変数を初期化する_ZipFolder”: [
“Succeeded”
]},
“metadata”: {
“operationMetadataId”: “02696254-ae57-421f-9cb2-12ff7d998a76”
}
}
}

おわりに

今回は2年ほど前に有識者の方の実装をまねて作ってみて改良までしたけど、内容の把握をしっかりしてない点とトリッキーな実装な点で実際に採用はせずに日の目を浴びなかったフローを記事にしてみました。
(容量の上限(90MBほど)はあるし、なんかあっても解消できないかもなので提案しなかった)

とはいえ、Zipファイルでまとめて落とせないのかなーという声はよく聞いたりもするので、限定的な利用でも構わないから使いたいという方もいるかなとおもい作り方を記事にしました。

しっかりしたシステムの実運用の場合はサードパティ製のコネクタを使用するほうが安全かと思います。
また、そもそもZipでダウンロードしなくても、SPOやOneDriveにフォルダにまとめてあげて、
そのパスに遷移できるようにしておいたら標準機能の手動操作でZipダウンロードできるので、
そういう運用でやってもらう方が良いのかなとも思います。

今回の実装を使う場合でも、将来的に対応できなくなったりイレギュラーなケースの場合は、
直接SPO参照してもらうなどの方法で代替えが効く範囲で取り入れていただくのが良いかなーと思います。
ご自身のご責任の範囲でご利用ご参考ください。それでは!

この記事は役に立ちましたか?

もし参考になりましたら、下記のボタンで教えてください。

ヨウセイ

ヨウセイ

一般職からSharePoint、C#、.NET系技術者へ、そこからPower App、Power Automate技術者へと転身。 ワンランク上のおっさんはPower Appsでシステム開発が出来る〜! qiitaや自社HPでも技術ブログを書いていました。

関連記事

コメント

この記事へのコメントはありません。

CONTENTS