【kintone/PowerShell】kintoneアプリの添付ファイルを一括ダウンロードしたい！

# はじめに

kintoneアプリの添付ファイルを一括でお引越ししたいシチュエーションがありました
使いやすいものが見つからなかったので作ってみました

動作や動作結果は保証しませんので、自己責任で実行ください

## やりたいこと

添付ファイルをローカルに一括ダウンロードしたい
レコード数が1万件超えのアプリでも動かしたい

## 前提条件

* ログインユーザーが閲覧できるレコードのみダウンロード可能です

## 実行環境

Windows 10

```powershell:PowerShellのバージョン
> $psversiontable

Name                           Value
----                           -----
PSVersion                      5.1.19041.1320
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.19041.1320
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
```

## 実行方法

1. ```kintoneDownloadAllFiles.ps1``` の定数ブロックの値を使用する環境に合わせて変更します
1. テスト実行したいときは、```$isTestRun = $true``` にします
1. PowerShellでこのファイルの存在する階層に移動し、"kintoneDownloadAllFiles.ps1"と入力し、実行します

## ソースコード（PowerShell）

```powershell:kintoneDownloadAllFiles.ps1
Add-Type -AssemblyName System.Web

############################################
###      説明                            ###
############################################

<#
    ■この処理について
        ・アプリに保存された1添付フィールドのファイルすべてをダウンロードし、"~/Downloads"へレコード番号ごとのフォルダへ保存します
        ・レコード数が1万件を超えるアプリにも対応します
        ・Windows 10で動作することを確認しました
    ■前提条件
        ・閲覧権限のないレコードの添付ファイルは取得されません
    ■使い方
        ・定数ブロックに記載された変数を使用する環境に合わせて変更します
        ・PowerShellでこのファイルの存在する階層に移動し、"kintoneDownloadAllFiles.ps1"と入力し、実行します
        ・定数ブロックの$isTestRunを"$true"にすると5レコードのみダウンロードするテスト実行を行います
    ■注意点
        ・添付ファイルが多い場合、処理に時間がかかります。通信環境のよいところで実行してください
#>

############################################
###      定数                            ###
############################################

$kintoneSubDomainName  = "subdomain-name"         # kintoneのサブドメイン名を指定します
$userName              = $Env:KintoneUserName     # kintoneのユーザー名を環境変数(KintoneUserName)に指定します
$userPassword          = $Env:KintonePassword     # kintoneのパスワードを環境変数(KintonePassword)に指定します
$appId                 = "0000"                   # ファイルをダウンロードしたいアプリのIDを指定します
$fieldNameAttachedFile = "添付"                   # ダウンロードしたい添付フィールドのフィールドコードを指定します
$fieldNameRecordNumber = "レコード番号"            # kintoneレコードのレコード番号のフィールドコードを指定します（通常、デフォルトで"レコード番号"が指定されています）
$isTestRun             = $false                   # 動作を確認したいときに"$true"にすると、5レコード分の添付ファイルのみダウンロードします

############################################
###      事前準備                         ###
############################################

#kintoneドメイン
$kintoneDomain = "https://${kintoneSubDomainName}.cybozu.com"

# Basic認証文字列作成
$basicAuthStrByte = ([System.Text.Encoding]::Default).GetBytes("${userName}:${userPassword}")
$basicAuthStrEncode = [Convert]::ToBase64String($basicAuthStrByte)

# ヘッダー作成
$headers = @{}
$headers.Add("X-Cybozu-Authorization", $basicAuthStrEncode)
$headers.Add("Authorization", "Basic ${basicAuthStrEncode}")

############################################
###      関数定義                         ###
############################################

# 対象アプリのレコード数を取得する
function getKintoneTotalCountNum ($APP_ID) {
    $recordGetTotalCountQueryEncode = [System.Web.HttpUtility]::UrlEncode("limit 1")
    $recordGetTotalCountReqUrl = "${kintoneDomain}/k/v1/records.json?app=${APP_ID}&totalCount=true&query=${recordGetTotalCountQueryEncode}"
    $recordGetTotalCountResponse = Invoke-RestMethod -Uri $recordGetTotalCountReqUrl -Method Get -Headers $headers
    $recordGetTotalCountResponse | ConvertTo-Json | Out-Null
    
    return [int]$recordGetTotalCountResponse.totalCount
}

############################################
###      メイン処理                       ###
############################################

# 全レコード数を確認し、レコード取得処理のAPI呼び出し回数を決定する（→$numRecordsGetReqRun）
$numAllRecords = getKintoneTotalCountNum $appId
$maxLimitRecordsPerRequest = 500
$numRecordsGetReqRun = [math]::ceiling($numAllRecords / $maxLimitRecordsPerRequest)

# 全レコード情報の添付ファイルの情報をあとで$fileListに入れる
$fileList = [System.Collections.ArrayList]::new()

# 全レコード取得に必要なだけループ
$indexRecordsGetStart = 1
$testRunRecordsNum = 5
for ($i = 0; $i -lt $numRecordsGetReqRun; $i++) {

    # レコードを取得
    $recordsGetQuery = "${fieldNameRecordNumber} >= ${indexRecordsGetStart} order by ${fieldNameRecordNumber} asc limit 500"
    $recordsGetQueryEncode = [System.Web.HttpUtility]::UrlEncode($recordsGetQuery)
    $recordsGetReqUrl = "${kintoneDomain}/k/v1/records.json?app=${appId}&totalCount=true&query=${recordsGetQueryEncode}"
    $recordsGetResponse = Invoke-RestMethod -Uri $recordsGetReqUrl -Method Get -Headers $headers
    $recordsGetResponse | ConvertTo-Json | Out-Null

    # レコードをループ
    for ($j = 0; $j -lt $recordsGetResponse.records.Length; $j++) {
        if ($j -eq $testRunRecordsNum -and $isTestRun) {break}
        $record = $recordsGetResponse.records[$j]
        if (-not $record.$fieldNameAttachedFile.value.Length) {continue}

        # レコードの各添付ファイルの情報を取得
        foreach ($file in $record.$fieldNameAttachedFile.value) {
            [void]$fileList.Add(@{
                fileKey=$file.fileKey; 
                name=$file.name; 
                contentType=$file.contentType; 
                size=$file.size;
                recordNumber=$record.$fieldNameRecordNumber.value;})
        }
    }
    $indexRecordsGetStart += 500
    if($isTestRun) {break}
}

$fileList.Count

# Downloadsフォルダの存在確認
$downloadsFolderPath = "~\Downloads"
If(!(test-path $downloadsFolderPath)) {
    New-Item -ItemType Directory -Force -Path $downloadsFolderPath
}

# ファイルをダウンロード
foreach ($file in $fileList) {
    $file
    $fileKey = $file.fileKey
    $fileName = $file.name
    $recordNumber = $file.recordNumber
    $fileGetReqUrl = "${kintoneDomain}/k/v1/file.json?fileKey=${fileKey}"
    $downloadsFolderPath = "~\Downloads\${recordNumber}"
    If(!(test-path $downloadsFolderPath)) {
        New-Item -ItemType Directory -Force -Path $downloadsFolderPath
    }
    Invoke-RestMethod -Uri $fileGetReqUrl -Method Get -Headers $headers -OutFile ${downloadsFolderPath}\${fileName}
}

```