Salesforceの添付ファイルをkintoneに移行する具体的な方法について

Salesforceの添付ファイルをkintoneのアプリに移行する方法です。
移行方法はいろいろあるかと思いますが[^1]、今回はできるだけOSに標準でインストールされている機能で移行してみます。
シェルスクリプトはよく分かってないので書き方に問題があるかもしれませんが、その辺りはご指摘いただけると助かります。


# 動作環境

* macOS 10.15.7
* bash
* awk version 20070501
* nkf Version 2.1.5 (2018-12-15)

# 前提条件

## ディレクトリとファイルの構成

* Attachments フォルダ：ファイル名が Salesforce ID[^2] として付与された添付ファイルの実体が入っている想定。
* Attachment.csv ファイル：添付ファイルのSalesforce IDとファイル名の対応情報や作成日などの情報が入っているファイル。文字コードはShift-JISの想定。
* sfid2name.sh シェルスクリプト：Salesforce IDからファイル名に変換するシェルスクリプト
* sf2kin.sh シェルスクリプト：kintone読み込み用のCSVファイルを生成するシェルスクリプト

```
$ tree -L 3
.
├── Attachment.csv
├── Attachments
│   ├── AAAAAAAAAAAAAAAAA1
│   ├── AAAAAAAAAAAAAAAAA2
│   ├── AAAAAAAAAAAAAAAAA3
│   ├── BBBBBBBBBBBBBBBBB1
│   └── BBBBBBBBBBBBBBBBB2
├── sfid2name.sh
└── sf2kin.sh
```

## Attachment.csv

```
$ cat Attachment.csv | nkf -Sw
"Id","IsDeleted","ParentId","AccountId","Name","IsPrivate","ContentType","BodyLength","BodyLengthCompressed","OwnerId","CreatedDate","CreatedById","LastModifiedDate","LastModifiedById","SystemModstamp","Description","FeedItemId"
"AAAAAAAAAAAAAAAAA1","0","00000000000000000A","00000000000000001A","サンプル.pdf","0","application/pdf","73499","62842","000000000000000000","2020-03-01 04:01:20","000000000000000000","2020-03-01 04:01:20","000000000000000000","2020-04-29 10:28:16","","000000000000000000"
"AAAAAAAAAAAAAAAAA2","0","00000000000000000B","00000000000000002A","sample.pdf","0","application/pdf","1416275","1222803","000000000000000000","2020-03-02 10:39:41","000000000000000000","2020-03-02 10:39:41","000000000000000000","2020-04-29 10:31:17","","000000000000000000"
"AAAAAAAAAAAAAAAAA3","0","00000000000000000C","00000000000000002A","sample.pdf","0","application/pdf","40955","32863","000000000000000000","2020-03-08 14:06:22","000000000000000000","2020-03-08 14:06:22","000000000000000000","2020-04-29 10:28:08","","000000000000000000"
"BBBBBBBBBBBBBBBBB1","0","00000000000000000D","00000000000000000A","サンプル.pdf","0","application/pdf","53491","45280","000000000000000000","2020-03-11 08:43:22","000000000000000000","2020-03-11 08:43:22","000000000000000000","2020-04-29 10:28:14","","000000000000000000"
"BBBBBBBBBBBBBBBBB2","0","00000000000000000E","00000000000000003A","サンプル.pdf","0","application/pdf","1498090","1324863","000000000000000000","2020-03-16 09:44:47","000000000000000000","2020-03-16 09:44:47","000000000000000000","2020-04-29 10:31:17","","000000000000000000"
```

# 処理１. Salesforceのファイルを元のファイル名に変換する

第１段階として、Salesforceの添付ファイルが入っているフォルダ（Attachmentsなど）内のファイル名を取得して、新規フォルダに "Salesforce ID/添付ファイル名" に名前を変換してコピーします。


```sfid2name.sh
#! /bin/bash

# read Attachment.csv and
# convert Shift-JIS to utf-8
# if test ! -f "$1"; then
if [ ! -f "$1" ]; then
    echo "$1"'ファイルが存在しません'
    exit 1
fi
if [ ! -d "$2" ]; then
    echo "$2"'フォルダが存在しません'
    exit 1
fi

cat "$1" | \
nkf -Sw | \
awk -v attachments_dir="$2" -F, \
'
BEGIN {
    print attachments_dir
    upload_file_dir = attachments_dir "_file_upload_dir" 
    print upload_file_dir

    # make upload file dir
    system("if test ! -d ./"upload_file_dir"; then mkdir ./"upload_file_dir"; fi")

    # set file_key to hash
    while ("ls ./"attachments_dir";" | getline) {
        ls_out[$0] = ++i
    }
}
{
    if (NR != 1) {
        # $1: Salesforce file Id, $5: file name
        sfid = $1
        name = $5
        gsub(/"/,"",sfid)
        sfid2name_hash[sfid] = name
    }
}
END {
    for (i in ls_out) {
        print i, ls_out[i], sfid2name_hash[i]
        attachment_path = attachments_dir "/" i
        upload_path = upload_file_dir "/" i
        copy_dir = upload_file_dir "/" i
        to_path = upload_file_dir "/" i "/" sfid2name_hash[i]
        system("if test ! -d ./"upload_path"; then mkdir ./"copy_dir"; fi")
        system("if test -d ./"upload_path"; then cp ./"attachment_path" ./"to_path"; fi")
    }
}
'
```

## 実行結果

```
$ ./sfid2name.sh Attachment.csv Attachments
Attachments
Attachments_file_upload_dir
AAAAAAAAAAAAAAAAA1 1 サンプル.pdf
AAAAAAAAAAAAAAAAA2 2 sample.pdf
AAAAAAAAAAAAAAAAA3 3 sample.pdf
BBBBBBBBBBBBBBBBB1 4 サンプル.pdf
BBBBBBBBBBBBBBBBB2 5 サンプル.pdf
```

```
├── Attachments
│   ├── AAAAAAAAAAAAAAAAA1
│   ├── AAAAAAAAAAAAAAAAA2
│   ├── AAAAAAAAAAAAAAAAA3
│   ├── BBBBBBBBBBBBBBBBB1
│   └── BBBBBBBBBBBBBBBBB2
├── Attachments_file_upload_dir
│   ├── AAAAAAAAAAAAAAAAA1
│   │   └── サンプル.pdf
│   ├── AAAAAAAAAAAAAAAAA2
│   │   └── sample.pdf
│   ├── AAAAAAAAAAAAAAAAA3
│   │   └── sample.pdf
│   ├── BBBBBBBBBBBBBBBBB1
│   │   └── サンプル.pdf
│   └── BBBBBBBBBBBBBBBBB2
│       └── サンプル.pdf
```


# 処理２. kintoneのファイル読み込み用CSVファイルを作成する

第２段階として、Salesforceの添付ファイル情報が入っているファイル（Attachment.csvなど）を読み込んで、kintoneに読み込む用のCSVファイルを作成します。


```sf2kin.sh
#!/bin/bash

if [ ! -f "$1" ]; then
    echo "$1"'ファイルが存在しません'
    exit 1
fi
if [ ! -d "$2" ]; then
    echo "$2"'フォルダが存在しません'
    exit 1
fi

cat "$1" | \
nkf -Sw | \
awk -v attachments_dir="$2" -F, \
'
BEGIN {
    upload_file_dir = attachments_dir "_file_upload_dir" 
    # set file_key to hash
    while ("ls ./"attachments_dir";" | getline) {
        ls_out[$0] = ++i
    }
}
{
    if (NR != 1) {
        # $1: Salesforce file Id
        sfid = $1
        gsub(/"/,"",sfid)
        sfid_hash[sfid] = $0
    }
}
END {
    kintone_csv_header = "\"FileId\",\"ParentId\",\"AccountId\",\"FileName\""
    file_upload_csv = "file_upload.csv"
    # file open
    print kintone_csv_header > file_upload_csv
    for (i in ls_out) {
        split(sfid_hash[i], rows, ",")
        file_dir = rows[1]
        file_name = rows[5]
        sub(/^"/,"",file_name)
        sub(/"$/,"",file_dir)
        print_row = rows[1] "," rows[3] "," rows[4] "," file_dir "/" file_name
        print print_row  >> file_upload_csv
    }
    close(file_upload_csv)
}
'
exit 0
```


## 実行結果

```
$ ./sf2kin.sh Attachment.csv Attachments
$ cat file_upload.csv
"FileId","ParentId","AccountId","FileName"
"AAAAAAAAAAAAAAAAA1","00000000000000000A","00000000000000001A","AAAAAAAAAAAAAAAAA1/サンプル.pdf"
"AAAAAAAAAAAAAAAAA2","00000000000000000B","00000000000000002A","AAAAAAAAAAAAAAAAA2/sample.pdf"
"AAAAAAAAAAAAAAAAA3","00000000000000000C","00000000000000002A","AAAAAAAAAAAAAAAAA3/sample.pdf"
"BBBBBBBBBBBBBBBBB1","00000000000000000D","00000000000000000A","BBBBBBBBBBBBBBBBB1/サンプル.pdf"
"BBBBBBBBBBBBBBBBB2","00000000000000000E","00000000000000003A","BBBBBBBBBBBBBBBBB2/サンプル.pdf"
```


## 説明

### cat
catコマンドはパラメータで指定したファイルを読み込んで、標準出力に出力する。
ファイルが指定されない場合はcatは標準入力から読み込む。

````
$ echo Hello | cat -n
     1  Hello
```

詳しくは`man cat`参照。

### nkf
nkfは文字コードを変換するプログラム。今回はSalesforceのエクスポートデータをShift-JISからmacOSで処理できるutf-8に変換するのに利用しています。

`nkf -Sw`はShift-JISを入力として、utf-8で出力する指定。
詳しくは`nkf --help`参照。

### awk -v attachments_dir="$2" -F, \

#### awk
`man awk`参照。

※awkについては別の記事にまとめています。
https://qiita.com/sy250f/items/60b6b114372f9b26c1f9

#### awk -F,
通常awkは読み込んだレコードを空白区切りとして分解するが、今回はカンマ区切りのCSVファイルが対象の為、`-F,`として区切り文字に`,`カンマを指定している。
awkは`"aaa,bbb"`のような形式には対応していないので別のプログラム(gawkやPerl,Rubyなど)を利用する。

#### awk -v attachments_dir="$2"
`-v`でシェル変数をawkの変数として渡すことができる。
2つ以上の変数を渡す場合は、`-v var1="$1" -v var2="$2"`とすれば良い。

#### BEGIN {}

`BEGIN`はawkの前処理に利用する。
今回は、変数の設定に利用。


### getline
今回getlineは `command | getline` の形式でシェルコマンド（ls）を受け取って、その結果を標準入力として受け取ります。
~~`ls`の結果でディレクトリ内のファイル名が出力される。出力されたファイル名はレコードとしてパイプに渡されて、FSにより（この場合は標準の空白）分解されるので、結果ファイル名ごとに処理することになる。~~

どうもそういうことではなくて、`ls`は出力先がターミナル以外の場合は、`ls -1`と同じ動作になるらしい。
`ls | cat`を試すと分かる。
つまり今回の場合、`ls`の出力先はパイプの為、`ls -1`を実行したことと同じになり、`ls`コマンドの結果は１ファイル１行として処理される為、getlineには１ファイルずつ渡されることになる。


```
    while ("ls ./"attachments_dir";" | getline) {
        ls_out[$0] = ++i
    }
```


# kintoneに添付ファイルをアップロードする
最後にkintoneに添付ファイルをアップロードします。

## kintoneアプリ
ファイルをアップロードするkintoneアプリを用意します。

![スクリーンショット 2021-03-26 13.04.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/a3abee65-e2a4-f5f4-3be5-41d126707b4e.png)

![スクリーンショット 2021-03-26 13.05.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/03b966bc-3f80-a1bd-23d3-9e1646590406.png)

## アップロード用シェルスクリプト

```upload.sh
#!/bin/bash

appId=<アプリID>
apiToken='<APIトークン>'
domain='<サブドメイン>'
attachmentFileDir='Attachments_file_upload_dir'
inputFilePath='file_upload.csv'

cli-kintone --import -a "$appId" -t "$apiToken" -d "$domain" -b "$attachmentFileDir" -f "$inputFilePath"
```

## 実行結果

![スクリーンショット 2021-03-26 13.09.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/96d5e219-c2ec-ec5e-7649-575fb7ca0b5d.png)


# Salesforceの作成日と最終更新日を含めてkintoneの日時形式に変換する
sedを使ってkintoneの日時形式に変換しています。

```sf2kin.sh
#!/bin/bash

if [ ! -f "$1" ]; then
    echo "$1"'ファイルが存在しません'
    exit 1
fi
if [ ! -d "$2" ]; then
    echo "$2"'フォルダが存在しません'
    exit 1
fi

cat "$1" | \
nkf -Sw | \
awk -v attachments_dir="$2" -F, \
'
BEGIN {
    upload_file_dir = attachments_dir "_file_upload_dir" 
    # set file_key to hash
    while ("ls ./"attachments_dir";" | getline) {
        ls_out[$0] = ++i
    }
}
{
    if (NR != 1) {
        # $1: Salesforce file Id
        sfid = $1
        gsub(/"/,"",sfid)
        sfid_hash[sfid] = $0
    }
}
END {
    kintone_csv_header = "\"FileId\",\"ParentId\",\"AccountId\",\"FileName\",\"CreatedDate\",\"LastModifiedDate\""
    print kintone_csv_header
    for (i in ls_out) {
        split(sfid_hash[i], rows, ",")
        file_dir = rows[1]
        file_name = rows[5]
        created_date = rows[11]
        last_modified_date = rows[13]
        sub(/^"/,"",file_name)
        sub(/"$/,"",file_dir)
        print_row = rows[1] "," rows[3] "," rows[4] "," file_dir "/" file_name "," created_date "," last_modified_date
        print print_row
    }
}
' | \
sed \
'{
s/\(\"[12][0-9]\{3\}[-][01][0-9][-][0-3][0-9]\) \([0-2][0-9]:[0-5][0-9]\):[0-5][0-9]/\1T\2Z/g
}
'\
> file_upload_"`date +%Y%m%d%H%M%S`".csv
exit 0
```

.env・・・環境変数

```.env
APPID=<アプリID>
APITOKEN='<APIトークン>'
SUBDOMAIN='<サブドメイン>'
```

```upload.sh
#!/bin/bash

source ./.env
if [ ! -f "$1" ]; then
    echo "$1"'ファイルが存在しません'
    exit 1
fi
if [ ! -d "$2" ]; then
    echo "$2"'フォルダが存在しません'
    exit 1
fi
inputFilePath="$1"
attachmentFileDir="$2"

cli-kintone --import -a "$APPID" -t "$APITOKEN" -d "$SUBDOMAIN" -b "$attachmentFileDir" -f "$inputFilePath"
```


```
$ ./upload.sh file_upload_20210327203737.csv Attachments_file_upload_dir
[2021-03-27 23:13:37]: Start from lines: 1 - 6 => SUCCESS
[2021-03-27 23:13:37]: DONE
```

![スクリーンショット 2021-03-27 23.14.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214523/50b4178a-e7d1-3697-202a-5b74996958a2.png)


# 添付ファイル名にカンマが入っている場合の対応
ダブルクォーテーションで囲んだフィールドの中身にカンマが入っている場合がある。
その場合は、標準の awk では処理が難しそうなので gawk を利用する。

```
$ echo '"aaa","bbb,ccc","ddd"' | awk -F, '{print $2}'
"bbb
```

```
$ echo '"aaa","bbb,ccc","ddd"' | gawk -v FPAT='([^,]+)|(\"[^\"]+\")' '{print $2}'
"bbb,ccc"
```


# 参考

* https://help.salesforce.com/articleView?id=000315637&type=1&mode=1
* https://qiita.com/eumesy/items/3bb39fc783c8d4863c5f
* https://www.gnu.org/software/gawk/manual/gawk.html


[^1]: awk以外にも、Perl,Ruby,Pythonなども良いかと思います。
[^2]: Salesforce ID は Salesforceで自動的に付与される18桁の英数字の番号のこと