プリザンターに登録した添付ファイルのデータを全て取得したい

# はじめに
なんとなく「プリザンターのデータベース内にある添付ファイルを取りたい」という好奇心で調べた結果、プログラムで出力した、という内容となります。

# プリザンターに登録した添付ファイルを取得するには
プリザンターで作成したテーブルを別環境へコピーする時サイトパッケージ機能のエクスポートを利用します。
このエクスポート機能は制限事項として**添付ファイルおよび画像はエクスポートできません。**
デフォルトの設定だとテーブルのバイナリに格納されてしまうので、バイナリデータから復元するツールをC#で作ってみました。

https://www.pleasanter.org/ja/manual/site-package-export

# 構成
* OS：windows 11 Pro
* DB：SQL Server 2019
* プリザンター：バージョン 1.4.6.4
* C#：.NET 8
* Visual Studio 2022

# プリザンターの添付ファイルの場所
デフォルトの設定では添付ファイルはBinariesテーブルに格納されています。
また、添付ファイルをローカルフォルダに変更する事も可能です。

https://pleasanter.org/ja/manual/table-management-local-folder-limit-size

#### SQL文にして必要そうな所をコメント
こんな感じだと思います。

```SQL
SELECT
 Bin         -- バイナリデータが格納されている列
,FileName    -- ファイル名（プリザンター上からカメラ等を起動して撮った写真には拡張子が無い？）
,Extension   -- 拡張子（カメラ等で起動して撮ったものには拡張子がないと思われる）
,Size        -- ファイルサイズ
,ContentType -- よくあるContentType
FROM Binaries
WHERE Title IS NOT NULL　-- タイトルがnullのものは初期値
```

# プログラム仕様
プログラム直下に**Binaries**フォルダを作成し、テーブルの中にあるバイナリデータをファイルに変換して格納していきます。

# プログラムの作成
Visual Studio 2022を起動し**新しいプロジェクトの作成**を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119455/3be9e3f0-fcfd-de96-40f6-08c37e5de7a9.png)

コンソールアプリ（C#）を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119455/6bfb1235-bd3c-cbbc-971a-c9df3312dd0c.png)

プロジェクト名を指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119455/57307241-14df-f717-043d-4797fa277522.png)

.NET8.0を指定しプロジェクトを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119455/6ad74e63-0217-5659-53b5-48cea6d52c37.png)

ソリューションエクスプローラーからProgram.csを選択し、編集します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119455/32af967b-39bd-cc31-a56b-323facd1cfbe.png)


# 実際のプログラム
コンソールアプリケーションを想定しているので「実行すれば動く」という感じになっています。
無駄な処理を書かないようにしようと試みましたが、結構な長さになってしまいました。

```Program.cs
using Microsoft.Data.SqlClient;

class Program
{
    static void Main(string[] args)
    {
        // 接続文字列
        string connectionString = "ここに接続文字列を入力してください";

        // 添付ファイルを出力するディレクトリ（プログラム実行直下）
        string outputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Binaries");

        try
        {
            // フォルダが存在しない場合は作成
            if (!Directory.Exists(outputDirectory))
            {
                Directory.CreateDirectory(outputDirectory);
            }

            // SQL Serverからデータを取得
            string query = @"SELECT Bin, FileName, Extension, Size, ContentType FROM Binaries WHERE Title IS NOT NULL";

            using (SqlConnection connection = new SqlConnection(connectionString))
            {
                connection.Open();
                using (SqlCommand command = new SqlCommand(query, connection))
                using (SqlDataReader reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        // バイナリデータ、ファイル名、拡張子を取得
                        byte[] fileData = (byte[])reader["Bin"];
                        string fileName = reader["FileName"].ToString();
                        string extension = reader["Extension"].ToString();
                        string contentType = reader["ContentType"].ToString();

                        // 拡張子がない場合は拡張子を付ける
                        if (string.IsNullOrWhiteSpace(extension))
                        {
                            if (contentType == "image/jpeg")
                            {
                                fileName += ".jpeg";
                            }
                        }

                        // 同一の名称がある場合は連番を付ける
                        fileName = HandleDuplicateFileName(outputDirectory, fileName);

                        // フルパスを組み立てる
                        string filePath = Path.Combine(outputDirectory, fileName);

                        // ファイルを書き込む
                        File.WriteAllBytes(filePath, fileData);
                        Console.WriteLine($"ファイルを保存しました: {filePath}");
                    }
                }
            }
        }
        catch (UnauthorizedAccessException ex)
        {
            Console.WriteLine($"ファイルアクセス権限エラー: {ex.Message}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"エラーが発生しました: {ex.Message}");
        }

        Console.WriteLine("終了。");
    }

    // 重複するファイル名に連番を付ける
    private static string HandleDuplicateFileName(string directory, string fileName)
    {
        // 拡張子とファイル名を分割
        string extension = Path.GetExtension(fileName);
        string originalFileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);

        int count = 1;

        // ファイル名が重複している場合、拡張子の前に数字を付加する
        while (File.Exists(Path.Combine(directory, fileName)))
        {
            fileName = $"{originalFileNameWithoutExtension}_{count}{extension}";
            count++;
        }

        return fileName;
    }

}

```

# 作成中に感じた感想
期待通りのプログラム動作になりましたが、全データを網羅していないので不足感がありました。様々なデータを入力して検証していければと思います。

次はテーブル毎に出力する等の制御可能なツールができないか検討してきたいと思います。

また、プリザンターがSQL ServerだけではなくPostgreSQLも対応しているので単純なSQLで作ってみましたが、環境がないため動作するか検証できていません（ツールを作るうえで複数の環境が必要だなと感じました）