[Salesforce] レコードに添付したファイルの一括ダウンロードボタンを作成する

この記事では「添付ファイル」はAttachmentではなく、ContentVersionのことを指しています。

## Salesforce上のファイルをダウンロードするURLについて

:::note warn
この記事で紹介するサーブレットURLは公式にサポートされているものではなく、予告なく変更される可能性があります。
ファイル名に日本語が含まれると、ファイル名はSalesforceIDに変換されます。
:::

Salesforce上のファイルをダウンロードするURLが存在しています。それが下記の2つです。

- `/sfc/servlet.shepherd/version/download/{ContentVersionId}`
- `/sfc/servlet.shepherd/document/download/{ContentDocumentId}`

idをスラッシュで区切って複数渡すことで複数ファイルまとめてZipファイルでダウンロードできます。

- `/sfc/servlet.shepherd/version/download/068XXXXXXXXXXXXXXX/068YYYYYYYYYYYYYYY`
- `/sfc/servlet.shepherd/document/download/069XXXXXXXXXXXXXXX/069YYYYYYYYYYYYYYY`

これを利用して、レコードに紐づくファイルをLWC、Aura、Visualforceなどで一括でダウンロードする機能が開発可能です。

※ただし、コンテンツドキュメントのファイル名に日本語が含まれると、ファイル名がSFIDに変換されてダウンロードされてしまいます。

## 実装例

レコードに添付されているファイルをZIPファイルで一括ダウンロードするクイックアクションの例です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125621/fa4c2836-b487-b923-33bf-70884b973794.png)


```html
<template>
    <lightning-quick-action-panel header="ファイルダウンローダー">
        <template lwc:if={fileListSize}>
            <p>{fileListSize}個の添付ファイルをダウンロードします。よろしいですか？</p>
            <div slot="footer">
                <lightning-button variant="neutral" label="キャンセル" onclick={handleClickCancel}></lightning-button>
                <lightning-button variant="brand" label="OK" class="slds-m-left_x-small" onclick={handleClickOk}></lightning-button>
            </div>
        </template>
        <template lwc:else>
            <p>添付ファイルが見つかりません。</p>
            <div slot="footer">
                <lightning-button variant="neutral" label="キャンセル" onclick={handleClickCancel}></lightning-button>
            </div>
        </template>
    </lightning-quick-action-panel>
</template>
```

```js
import { LightningElement, api, wire } from "lwc";
import { NavigationMixin } from "lightning/navigation";
import { CloseActionScreenEvent } from "lightning/actions";
import { gql, graphql } from "lightning/uiGraphQLApi";

const FILES_DOWNLOAD_URL = "/sfc/servlet.shepherd/document/download";
const QUERY = gql`
  query GetFiles($recordId: ID!) {
    uiapi {
      query {
        ContentDocumentLink(where: { LinkedEntityId: { eq: $recordId } }) {
          edges {
            node {
              ContentDocument {
                Id
              }
            }
          }
        }
      }
    }
  }
`;

export default class MassFileDownloader extends NavigationMixin(LightningElement) {
  @api recordId;
  files;

  get fileListSize() {
    return this.files?.length;
  }

  @wire(graphql, {
    query: "$recordQuery",
    variables: "$variables"
  })
  queryGraphql(value) {
    this.wiredData = value;
    const { errors, data } = value;
    if (data) {
      const records = data.uiapi?.query?.ContentDocumentLink?.edges?.map((record) => record.node.ContentDocument);
      this.files = records;
    } else if (errors) {
      console.error("GraphQL error", errors);
    }
  }

  get recordQuery() {
    if (!this.recordId) {
      return undefined;
    }
    return QUERY;
  }

  get variables() {
    return {
      recordId: this.recordId
    };
  }

  handleClickOk() {
    if (this.files) {
      const downloadUrl = this.files.reduce((previous, current) => (previous += `/${current.Id}`), FILES_DOWNLOAD_URL);
      this.navigate(downloadUrl);
    }
  }

  handleClickCancel() {
    this.dispatchEvent(new CloseActionScreenEvent());
  }

  navigate(url) {
    this[NavigationMixin.Navigate](
      {
        type: "standard__webPage",
        attributes: { url }
      },
      true
    );
  }
}
```

```xml
<?xml version="1.0"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>62.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordAction</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordAction">
            <actionType>ScreenAction</actionType>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
```

## Appendix

### PDFファイルでダウンロードする

クエリパラメータに`asPdf=true`を追加するとPDFとしてダウンロードされます。

### ファイルのプレビューを表示する

`/sfc/servlet.shepherd/version/renditionDownload?rendition={種別}&versionId={ContentVersionId}`

renditionに使用できる値は下記です。

- `SVGZ`
- `ORIGINAL_Jpg`
- `ORIGINAL_Png`
- `THUMB120BY90` - サムネイル
- `THUMB240BY180` - サムネイル
- `THUMB720BY480` - サムネイル

プレビューが表示されない場合は[こちら](https://help.salesforce.com/s/articleView?id=000387545&type=1)を参照してください。

## 参考
- [Building Content-Rich Visualforce Pages with Salesforce CRM Content | Salesforce Developers Blog](https://developer.salesforce.com/blogs/engineering/2014/04/building-content-rich-visualforce-pages-with-salesforce-crm-content)
- [files - Content Document download URL params - Salesforce Stack Exchange](https://salesforce.stackexchange.com/questions/203030/content-document-download-url-params)
- [画面クイックアクションの作成 | Developers](https://developer.salesforce.com/docs/platform/ja-jp/lwc/guide/use-quick-actions-screen.html)
- [コンテンツファイルのプレビューの問題 | Salesforce Help](https://help.salesforce.com/s/articleView?id=000387545&type=1)
- [ファイルのプレビューの再生成 | Salesforce Help](https://help.salesforce.com/s/articleView?id=sf.collab_admin_files_regenerate_preview.htm&type=5)