【Unity(C#)】ファイルの保存上限を設けて日付の古いファイルから順に消去する実装

##はじめに
【参考リンク】：[【Unity(C#)】テクスチャを画像データに変換して端末に保存、読み込み](https://qiita.com/OKsaiyowa/items/21a9ca438efbf605c52d)
↑前回の応用です。

やりたい事としては下記の画像のように
ファイルの保存上限を設けて、
上限を超えた場合は最も古いファイルから削除するという実装です。
![SaveLimitImage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/652511d0-9637-d5e5-ced8-4a055427ec3f.png)



##デモ
早速ですがデモです。
保存するファイルは画像ファイルとしました。
![Paint2DForQiita5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/d2225a24-7156-02a0-9a81-35ebf2d986fa.gif)

PC内の保存領域を確認したところ、
しっかりと最後の3枚が保存されていました。

![SaveImageCheckSS.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/5a1b0f85-22f8-da12-b0f0-9d51214aad77.png)


##コード
下記今回の処理の全文です。

```C#:適当なオブジェクトにアタッチ
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UniRx;
using UniRx.Triggers;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// テクスチャー ⇔ Png画像 の変換と保存と読み込み
/// </summary>
public class TexturePngConverter : MonoBehaviour
{
    [SerializeField] private Button _saveButton;
    [SerializeField] private Button _loadButton;
    [SerializeField] private Image _paintImage;
    [SerializeField] private Painter _painter;
    [SerializeField] private Transform _loadImagesParentTransform;

    private const string IMAGE_SAVE_FOLDER = "Image";
    private const string PNG = ".png";

    /// <summary>
    /// 限界保存枚数
    /// </summary>
    private const int UPPER_LIMIT_SAVE_PICTURE = 3;
    
    private void Start()
    {
        //セーブボタン
        _saveButton.OnPointerClickAsObservable()
            .Subscribe(_ =>
            {
                //保存
                ConvertToPngAndSave(GetSaveDirectoryPath(IMAGE_SAVE_FOLDER), GetSaveFilePath(IMAGE_SAVE_FOLDER, PNG));
                //リセット
                _painter.ResetTexture();
            }).AddTo(this);

        //ロードボタン
        _loadButton.OnPointerClickAsObservable()
            .Subscribe(_ => ConvertToTextureAndLoad(GetSaveDirectoryPath(IMAGE_SAVE_FOLDER))).AddTo(this);
    }

    /// <summary>
    /// 保存先のファイルのパス取得
    /// </summary>
    /// <param name="folderName">区切りのフォルダ名</param>
    /// <param name="fileType">拡張子</param>
    /// <returns>保存先のパス</returns>
    private string GetSaveFilePath(string folderName, string fileType)
    {
        return GetSaveDirectoryPath(folderName) + DateTime.Now.ToString("yyyyMMddHHmmss") + fileType;
    }

    /// 保存先のディレクトリのパス取得
    /// </summary>
    /// <param name="folderName">区切りのフォルダ名</param>
    /// <returns>保存先のパス</returns>
    private string GetSaveDirectoryPath(string folderName)
    {
        string directoryPath = Application.persistentDataPath + "/" + folderName + "/";

        if (!Directory.Exists(directoryPath))
        {
            //まだ存在してなかったら作成
            Directory.CreateDirectory(directoryPath);
            return directoryPath;
        }

        return directoryPath;
    }

    /// <summary>
    /// "ディレクトリ配下のファイル"が全て入ったリストを返す
    /// 最も古いファイルが[0]番目
    /// </summary>
    /// <param name="directoryName">取得したいファイル群の親ディレクトリ</param>
    /// <returns>指定したディレクトリ配下のファイルが全て入ったリスト</returns>
    private List<string> GetAllFileFromDirectory(string directoryName)
    {
        //古いものが先頭にくるようにファイルをソート
        List<string> imageFilePathList = Directory
            //Imageディレクトリ内の全ファイルを取得
            .GetFiles(directoryName, "*", SearchOption.AllDirectories)
            //.DS_Storeは除く
            .Where(filePath => Path.GetFileName(filePath) != ".DS_Store")
            //日付順に降順でソート
            .OrderBy(filePath => File.GetLastWriteTime(filePath).Date)
            //同じ日付内で時刻順に降順でソート
            .ThenBy(filePath => File.GetLastWriteTime(filePath).TimeOfDay)
            .ToList();

        return imageFilePathList;
    }

    /// <summary>
    /// 画像に変換＆保存
    /// 上限保存数をチェック
    /// </summary>
    /// <param name="directoryPath">保存数をチェックするディレクトリ</param>
    /// <param name="fileSavePath">保存先のパス</param>
    private void ConvertToPngAndSave(string directoryPath, string fileSavePath)
    {
        //指定したディレクトリー配下のファイルが全て入ったリストを取得
        List<string> imageFilePaths = GetAllFileFromDirectory(directoryPath);

        //ファイル数の上限をチェック
        if (imageFilePaths.Count >= UPPER_LIMIT_SAVE_PICTURE)
        {
            //上限に達していた場合、最も古いファイルを削除
            File.Delete(imageFilePaths[0]);
        }

        //Pngに変換
        byte[] bytes = _paintImage.sprite.texture.EncodeToPNG();
        //保存
        File.WriteAllBytes(fileSavePath, bytes);
    }

    /// <summary>
    /// テクスチャに変換＆読み込み
    /// </summary>
    /// <param name="directoryPath">ロードしたいファイル群の親ディレクトリ</param>
    private void ConvertToTextureAndLoad(string directoryPath)
    {
        List<Image> imageList = new List<Image>();

        //ロード後、複数枚表示するためのImageリスト作成
        foreach (Transform child in _loadImagesParentTransform)
        {
            Image childImage = child.gameObject.GetComponent<Image>();

            if (childImage != null)
            {
                imageList.Add(childImage);
            }
        }
        
        //指定したディレクトリー配下のファイルが全て入ったリストを取得
        List<string> imageFilePaths = GetAllFileFromDirectory(directoryPath);

        //インデックス用カウンター
        int count = 0;

        //ファイルのリストから古い順にロードしてImageに適用
        foreach (string imageFilePath in imageFilePaths)
        {
            Debug.Log(imageFilePath);
            //読み込み
            byte[] bytes = File.ReadAllBytes(imageFilePath);
            //画像をテクスチャに変換
            Texture2D loadTexture = new Texture2D(2, 2);
            loadTexture.LoadImage(bytes);
            //テクスチャをスプライトに変換
            imageList[count].sprite = Sprite.Create(loadTexture, new Rect(0, 0, loadTexture.width, loadTexture.height), Vector2.zero);
            //インデックス用カウンターを進める
            count++;
        }
    }
}
```

##ソート
今回は**古いフォルダの概念を保存した日時(秒単位)**にしています。
そのため、ディレクトリ内のファイルの中から
**最も古い保存日時のファイル**を取得する必要がありました。

そのために一度全ファイルを取得して
それぞれの保存先のパスを保存順にソートしています。

ロード時も同様に全ファイルのパスを取得し
`File.ReadAllBytes`の引数に渡しています。

下記リンクを参考にLINQを使って簡単に書くことができました。

【参考リンク】：[タイムスタンプ(作成日、変更日、最後に開いた日)を基準にファイルを古い順や新しい順にソート【C#】【LINQ】](https://kan-kikuchi.hatenablog.com/entry/Time_Stamp_Sort)

##DateTime.Now
`DateTime.Now`を使えば現在の日付と秒単位までの時刻を取得することができます。

今回の実装においては、
この日付と秒単位までの時刻をファイル名とすることで
ユニークなパスを生成しています。

ただ、そのままファイル名として与えた際に少し厄介なことがありました。

`Image/2020/09/21 17:12:43.png`のようにディレクトリの区切りとして解釈される
`/(スラッシュ)`が含まれてしまいます。

このまま保存処理を行おうとすると
**そんなディレクトリは存在しません**と怒られます。

ですので、下記のように引数に`"yyyyMMddHHmmss"`を渡しています。

```C#
DateTime.Now.ToString("yyyyMMddHHmmss")
``` 
こうすることで`Image/20200921171710.png`となり
秒数まで全て数字で取得することが可能となります。

【参考リンク】：[日付や時刻を文字列に変換するには？](https://www.atmarkit.co.jp/ait/articles/0408/27/news104.html)

---
2020/09/21 追記

`GetFiles(directoryName, "*", SearchOption.AllDirectories)`
の箇所を`"*"`→`"*.png"`等にした方が
想定外のファイルを除外することができるとコメント頂いたので
メモしときます　(ありがとうございます！)


##おわりに
モバイル端末で一気に5,60枚読み込んでも処理落ちは見られませんでした。
枚数が何百、何千となるといろいろと工夫が必要そうです。