vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【ASP.NET Core】任意のローカルファイルを読み込み・書き出し

はじめに

ASP.NET Core では Static なファイルはデフォルトで wwwroot に置きます。

が、今回は PC の任意の場所に置いたファイルを Static ファイルとして扱うことができるか、ということを試してみました。

ついでなのでクライアント( Unity )に画像を渡し、それを Texture として読み込んで表示してみることにします。

ローカルファイルにアクセスする

例えば Documents ディレクトリに置いたファイルに、ファイル名を指定してアクセスしてみます。

// Documents をルートディレクトリとして物理ファイルのプロバイダーを生成.
using(PhysicalFileProvider provider = new PhysicalFileProvider(@"C:\Users\example\Documents")) {
  // Documents > memo.txt のファイル情報を取得.
  IFileInfo fileInfo = provider.GetFileInfo(@"memo.txt");
  logger.LogInformation("name: " + fileInfo.Name + " exists: " + fileInfo.Exists + " directory?: " + fileInfo.IsDirectory);
}

実行した結果(ログの内容)は以下の通りです。

name: memo.txt exists: True directory?: False

今度はルートディレクトリだけを指定して、その下にあるディレクトリ、ファイルの情報を取得してみます。

なお該当のディレクトリには下記のようなファイル、ディレクトリがあるものとします。

ImageSample ← ルートディレクトリとして指定
  Lsample1.jpg
  Ldir1
    Ldir2
      Lsample2.jpg
    Lsample3.jpg

// ImageSample をルートディレクトリとして物理ファイルのプロバイダーを生成.
using (PhysicalFileProvider provider = new PhysicalFileProvider(@"C:\Users\example\Pictures\ImageSample")) {
  // サブディレクトリを指定する場合は GetDirectoryContents の引数で指定.
  IDirectoryContents contents = provider.GetDirectoryContents(string.Empty);
  foreach (IFileInfo fileInfo in contents){
      logger.LogInformation("name: " + fileInfo.Name + " exists: " + fileInfo.Exists + " directory? " + fileInfo.IsDirectory);
  }
}

実行した結果(ログの内容)は以下の通りです。

name: sample1.jpg exists: True directory?: False
name: dir1 exists: True directory?: True

対象のディレクトリだけが対象となっていて、それより下の階層の情報は取れていません。

全部取得してみる

このノリで、ルートディレクトリ以下のディレクトリ、ファイル情報をまとめて取ってみることにします。

またファイルは、画像ファイルのみをターゲットとしてみます。

まず情報を格納する構造体を用意します。

ImageFile.cs

public struct ImageFile {
    public string DirectoryPath { get; }
    public string FileName { get; }

    public ImageFile(string setDirectoryPath, string setFileName) {
        DirectoryPath = setDirectoryPath;
        FileName = setFileName;
    }
}

別にクラスでもいいのですが、 More Effective C# に従って、データを格納するものは構造体にすることにします。

public List< ImageFile> Load() {
  List< ImageFile> loadedFiles = new List< ImageFile>();

  using (PhysicalFileProvider provider = new PhysicalFileProvider(@"C:\Users\example\Pictures\ImageSample")) {
      loadedFiles.AddRange(
          Load(provider, @"\"));
  }
  // 取得した情報をログに出力してみる.
  foreach (ImageFile f in loadedFiles) {
      logger.LogInformation("directory: " + f.DirectoryPath + " file: " + f.FileName);
  }
  return loadedFiles;
}
private static List< ImageFile> Load(PhysicalFileProvider provider, string parentDirectory) {
    List< ImageFile> loadedFiles = new List< ImageFile>();

    foreach (IFileInfo fileInfo in provider.GetDirectoryContents(parentDirectory)) {
        if (fileInfo.IsDirectory) {
            // ディレクトリならその下にあるファイルを取得する.
            loadedFiles.AddRange(
                Load(provider, parentDirectory + fileInfo.Name + @"\"));
        }
        // IFileInfo では拡張子を取れないようなので、正規表現でファイルを選別.
        else if(Regex.IsMatch(fileInfo.Name, "(jpg|png)$")) {
            loadedFiles.Add(
                new ImageFile(parentDirectory, fileInfo.Name));
        }
    }
    return loadedFiles;
}

ログの出力結果は以下の通りです。

directory: \dir1\dir2\ file: sample3.jpg
directory: \dir1\ file: sample2.jpg
directory: \ file: sample1.jpg

結果の通り、階層が深い方から順に並ぶことになるので、 DirectoryPath でソートした方が良いかもしれません。

取得した情報を JSON として扱う

取得した List< ImageFiles> ですが、クライアント側に渡せるよう、 JSON として扱えるようにしたいと思います。

例えば GET リクエストでアクセスしたときに、 JSON で返す方法は下記のようにできます。

HomeController.cs

namespace WebApplication1.Controllers {
  public class HomeController : Controller {
    ~省略~
    [Route("/getfileinfo")]
    [Produces("application/json")]
    public List< ImageFile> Index() {
      // C:\Users\example\Pictures\ImageSample のデータを取得して返す.
      return Load();
    }
  }
}

ただこの場合、アクセスするたびにデータを作ることになるため、効率が悪いです。

そのため、データを読み込んだ後 JSON ファイルとして一旦保存しておき、アクセスされたときはそれを渡すようにしてみます。

JSON に変換する

DataContractJsonSerializer を使うことで JSON に変換することができます。

まず DataContractJsonSerializer で扱うことができるよう、 ImageFile を変更します。

ImageFile.cs

using System.Runtime.Serialization;

namespace WebApplication1.FileAccessers {
    [DataContract]
    public struct ImageFile {
        [DataMember]
        public string DirectoryPath { get; private set; }
        [DataMember]
        public string FileName { get; private set; }

        public ImageFile(string setDirectoryPath, string setFileName) {
            DirectoryPath = setDirectoryPath;
            FileName = setFileName;
        }
    }
}

注意が必要な点は2つです。

  1. クラスに DataContract 、 変数(プロパティ) に DataMember のアトリビュートをつける必要があります。
  2. 各プロパティに set が必要で、 get だけにしているとエラーになります( private でも OK )。

で、これを使って JSON に変換してみます。

~省略~
using System.Runtime.Serialization.Json;
using System.Text;
~省略~
    private byte[] Serialize(List< ImageFile> originalData) {

        using (MemoryStream ms = new MemoryStream()) {
            // List< ImageFile> を変換する場合は DataContractJsonSerializer の型も List< ImageFile> とする.
            DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(List< ImageFile>));

            serializer.WriteObject(ms, originalData);
            return ms.ToArray();
        }
    }
  }
}

ファイルを書き出すときのことを踏まえて byte[] で返していますが、 string でほしい場合は Encoding.UTF8.GetString(json, 0, json.Length) のように変換すれば OK です。

ファイルの書き出し

ではこの JSON をファイルとして書き出します。

ここでは FileStream を使います。

// JSON 変換した byte[] データ.
byte[] result = Serialize(loadedFiles);

using (FileStream stream = new FileStream(@"C:\Users\example\Pictures\ImageSample\createdjson.json", FileMode.Create)) {
    stream.Write(result, 0, result.Length);
}

特に ASP.NET Core でなくても変わらないですね。

ただ今回は同期的に書き込んでいますが、 WriteAsync にした方がよいかもしれません。

ローカルのディレクトリをStaticFilesに追加する

ここまで扱ってきたファイルを、外からも見えるようにしてみたいと思います。

その方法として、ローカルのディレクトリをStaticFilesに追加する、というものがあります。

Startup.cs

~省略~
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
  ~省略~
  // wwwroot を Static ファイルのルートとして設定する.
  app.UseStaticFiles();

  // ローカルの Pictures にある ImageSample をStatic ファイルのディレクトリとして追加.
  app.UseStaticFiles(new StaticFileOptions {
      FileProvider = new PhysicalFileProvider(
          Path.Combine(@"C:\Users\example\Pictures", "ImageSample")),
      RequestPath = "/img"
  });
  ~省略~
}
~省略~

こうすることで、例えば localhost:5XXX/img/sample1.jpg にアクセスすると C:\Users\example\Pictures\ImageSample\sample1.jpg が表示されるようになります。

アプリの発行後、外部からファイルを差し替えたい、といった場合に利用できそうです。

おわりに

案外簡単にアクセス可能にできるものなのだなぁ。というのが正直な感想です。

場合によってはルートディレクトリ内の特定のディレクトリ/ファイルだけアクセスさせないなどの措置も必要になりそうですね。

こちらについては別途調べて書き残そうかと思っています。

参照