Slackのデータエクスポートファイルから添付ファイルのみを一括DL

#0.前書き
qiita初カキコども・・・
＋αで、対象としてはあんましC#よくわからんという同期用に作ったドキュメントなのでご了承を。
書き方のノウハウ溜まったら、恥ずかしい文章とか構成変えたりするのでご了承を。

#1.背景
slackのフリープランを使用して、適当な個人＆同期用ワークスペースのチャンネルに考えなしにデータシートなどのpdfとかの技術資料をぶちこんでた今日この頃。
（dropboxなど使わなかったのは、ファイル＋メモや覚書が同時に書き込めるから）
で、その技術資料をローカルに持ってくる必要が出てきたんですが、数千件くらいデータがあって、それらをいちいち抜き出すのが非常にめんどくさい。
同期も僕もワークスペースの管理者ではあるので、メッセージデータのすべてをエクスポートは可能だが、ファイル自体は添付されていない。
「slack」+「エクスポートデータ」+「添付ファイルor抜き出す方法or一括DL」とかで調べても同期が理解できる目当ての情報が出てこない。
なので、エクスポートデータから、一括で添付ファイルをDLするサンプルプログラムを作ったので自分と同期への覚書として記すこととした。

ホントはAPIでチャンネルメッセージをすべて取得して、ファイルDL用URLを取得して1つずつ丁寧にDLするのが良いんですが、個別にTokenを発行する必要があるのと、jsonも英語リファレンスも読めない上にPOST/GETが理解出来ない同期の頭が爆発するので今回はエクスポートデータから抜き取る方式で行うワケです。

#2.環境および前提条件
とりあえず最低限の情報をざっくり箇条書き。

##2-1.開発環境
- Visual Studio 1019
- .NET Core 3.1
- コンソールアプリケーション
- Newtonsoft.json Ver 13.0.1

##2-2.Slackの前提条件
- メッセージの履歴をエクスポートできる権限を持ってる
- 取得したいメッセージがあるチャンネルがパブリックである

#3.Slackからデータのエクスポート
この項目をSlackから見つけ出して、データのエクスポートを行う。
`ワークスペースの設定　>　設定と権限　>　データのインポート/エクスポート`

どこにあるかわからんって人は下記URLを参考に。
[Slack help center - ワークスペースのデータをエクスポートする(2022/02/18 参照)](https://slack.com/intl/ja-jp/help/articles/201658943)

で、エクスポート画面では期間は全範囲のデータをエクスポートする。

**※注意**
1. チャンネルの個別指定はできない。取るときはワークスペース全体のファイルが一気に出力される。また、プライベート/DMのチャネルデータも取れない。諦めてもろて。素直にAPIを叩こう。
2. エクスポートされた内容はダウンロード後、自動的に削除される。鮮度が良いうちに取り込もう。
3. Tokenは後に取得する`url_private_download `という項目に付随してるので気にしないで。これも多分自動削除の対象。

#4.Slackデータエクスポートの形式について
詳細については、下記参照。
[Slack help center - Slack からエクスポートしたデータの読み方(2022/02/18 参照)](https://slack.com/intl/ja-jp/help/articles/220556107)

ただ、同期はjsonというものを理解できないので、とりあえず下記のクラスを準備してもろて。と伝えている。
~~(クラスは理解できるのに、なぜjsonは理解できないのか…)~~

必要なのは1のname(ファイル名)と2のurl_private_download(DL用URL)のみ。
ジェイソンくんを理解できてもらえることを信じて、他も記載している。

```c#:fileタグに含まれるオブジェクトクラス
public class files
{
    public string id { get; set; }
    public string created { get; set; }
    public string timestamp { get; set; }
    //必要1
    public string name { get; set; }
    public string title { get; set; }
    public string mimetype { get; set; }
    public string filetype { get; set; }
    public string pretty_type { get; set; }
    public string user { get; set; }
    public string editable { get; set; }
    public string size { get; set; }
    public string mode { get; set; }
    public string is_external { get; set; }
    public string external_type { get; set; }
    public string is_public { get; set; }
    public string public_url_shared { get; set; }
    public string display_as_bot { get; set; }
    public string username { get; set; }
    public string url_private { get; set; }
    //必要2
    public string url_private_download { get; set; }
    public string media_display_type { get; set; }
    public string thumb_pdf { get; set; }
    public string thumb_pdf_w { get; set; }
    public string thumb_pdf_h { get; set; }
    public string permalink { get; set; }
    public string permalink_public { get; set; }
    public string is_starred { get; set; }
    public string has_rich_preview { get; set; }
}
```

#5.DLするプログラムと結果
下記コードの肝は、jsonの構造を理解してなくても、文字列の形式さえあっていればファイルを読み出せることです。
つまり同期の頭が爆発せずに済むということです。
同期には、これを読み解いてjsonファイルの展開の利便性を知ってもらえればと思います。

```c#:メインプログラム
using Newtonsoft.Json;//ジェイソンくんを解体・新生させるのに必要
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;

public class Program
{
    //各ジェイソンくんが保存されているPath格納用
    static public string[] filePath;

    static void Main(string[] args)
    {
        //ターゲットとなるフォルダを指定して初期化
        filePath = Directory.GetFiles(@"C:\targetFilePath", "*", SearchOption.TopDirectoryOnly);
        Console.WriteLine("見つけたジェイソンくん数 : " + filePath.Length);

        //各ジェイソンくんを探して解析するループ
        for (int i = 0; i < filePath.Length; i++)
        {
            //対象ジェイソンくんを解体して各fileの中身をReplaseするリスト
            List<string> strFilesText = new List<string>();
            //対象ジェイソンくんにfileオブジェクトが存在するか？
            bool bExist = false;

            //対象ジェイソンくんデータ読み込み部分
            using (StreamReader sr = new StreamReader(filePath[i], Encoding.UTF8))
            {
                //最終行までぶん回す。
                while (sr.EndOfStream == false)
                {
                    //1行読み用
                    string strLine = string.Empty;

                    //1行目は"["という不要な文字があるので空読み。
                    strLine = sr.ReadLine();

                    //1行読み込んで、【"files":[】という部分あれば抜き取り開始。力技
                    if (strLine.Contains("\"files\": ["))
                    {
                        //ここに付け足して、1行1行丁寧にリストに格納する
                        string append = "";

                        while (true)
                        {
                            strLine = sr.ReadLine();

                            if(strLine.TrimStart() == "{" || strLine.TrimStart() == "},")
                            {
                                //始端、終端があればとりあえず{の始端をつける。
                                append = "{";
                            }
                            else
                            {
                                if(strLine.TrimStart() == "],")
                                {
                                    //filesタグの最後は],で終わるので、ここで抜ける。
                                    break;
                                }
                                else
                                {
                                    //それ以外は最初の空白削ってアペンド
                                    append += strLine.TrimStart();

                                    if (strLine.Contains("has_rich_preview"))
                                    {
                                        //要素の最後にhas_rich_previewがあれば新たなジェイソンくんが
                                        //作れた証左なのでReplaseリストに追加する。
                                        append += "}";
                                        strFilesText.Add(append);
                                        append = "{";
                                        //で、新しいジェイソンくんがいるフラグをONする。
                                        bExist = true;
                                    }
                                }
                            }
                        }
                    }
                }
            }

            if (bExist == true)
            {
                //新しいジェイソンくんがいる場合実行。

                //大量のジェイソンくんを格納するクラス用意
                List<files> jsondata = new List<files>();
                //舐めまわす
                foreach (string lines in strFilesText)
                {
                    try
                    {
                        //良いジェイソンくん(正常形式のjsonFile)は追加する
                        jsondata.Add(JsonConvert.DeserializeObject<files>(lines));
                    }
                    catch(Exception ex)
                    {
                        //悪いジェイソンくんは除外する
                        Console.WriteLine(ex);
                    }
                }

                //ジェイソンくんを舐めまわす
                foreach (files data in jsondata)
                {
                    //書き込み先のディレクトリを設定(今回は適当なPATH＋読み込んだジェイソンくんの名前　がファイル名になる)
                    string toDirectoryName = @"C:\replaceFilePath\" + Path.GetFileNameWithoutExtension(filePath[i]);

                    //DLするために接続するWebクライアント
                    using (var webClient = new WebClient())
                    {
                        //url_private_downloadタグのURLでstreamデータを取得。
                        using (MemoryStream ms = new MemoryStream(webClient.DownloadData(data.url_private_download)))
                        {
                            //streamをバイト配列にConvert
                            byte[] content = ms.ToArray();

                            //書き込み先のディレクトリを用意
                            Directory.CreateDirectory(toDirectoryName);
                            //書き込み先の新しいディレクトリ名＋読み込めたファイルの名前をパスとする。
                            string fileName = toDirectoryName + @"\" + data.name;

                            if (File.Exists(fileName))
                            {
                                //同一ファイルがあると上書きしちゃうので、適当にリネーム。今回は、milliSecを足して誤魔化す
                                fileName = toDirectoryName + @"\" + DateTime.Now.ToString("yyyyMMddHHmmssff") + "_" + data.name;
                            }

                            //で、一行ずつバイナリデータをファイルに書き込み
                            using (FileStream fs = File.Create(fileName))
                            {
                                fs.Write(content, 0, (int)content.Length);
                            }
                            Console.WriteLine("処理完了 : " + fileName);
                        }
                    }

                    //あんまり頻繁にアクセスすると怖いから10mSecのウェイトを入れる。
                    Thread.Sleep(10);
                }

            }
        }
    }
}
```

このコードを実行すれば、各日ごとにjsonが分かれているので日付ごとにフォルダ＋技術資料のpdfやらが保存される。
<font color="AntiqueWhite">やったねたえちゃん。</font>

#6.最後に
自分と同期用のメモ書きでも良いんですが、自分で調べた限りではAPIを使わずにSlackのエクスポートデータから添付ファイルを取得する方法は見つけだせなかったので、せっかくなのでqiitaに投稿しようかと思った次第。
同期も徐々にスキルアップすれば、こんな方法使わなくてもAPI使った方が早いと思ってくれるはず。