情報応用演習Ⅰ(2024)

【T10b】簡易ブログソフトウェアの作成 Part.Ⅳ ~ ファイルのアップロード/ダウンロード(3/7)

プロジェクトタイプ(注意: 本文参照)
プロジェクト名T10b
ソリューション名PIT10
注意
  • 本ページの作業内容は 前のページまでの続き になっていることに注意せよ.
    • 先に前のページまでをすべて読み,指示されている作業を済ませてから本ページを読むこと.
    • プロジェクトの作成作業については準備作業を参照せよ.

10b-3. ファイルのアップロード/ダウンロード

ウェブアプリケーションではファイルのアップロードやダウンロードの機能が求められるものも少なくない. X(Twitter)やFacebookといった商用のサービスなどはもちろんのこと,昔ながらの画像掲示板やもちろんウェブログなどでも ファイルを扱う機能を備えているものは一般的である. ウェブアプリケーションでファイルを扱う場合,最も気にしなければならないのは アップロードされたファイルをどこに置くか である. これには以下のようなシナリオがありうる.

  1. データベース内に保存する.
  2. ファイルシステムに直接保存する.
  3. オブジェクトストレージを利用する.

1.はリレーショナルデータベース内にファイルを保存する方法である. リレーショナルデータベース製品の中には任意のバイトストリーム(=バイナリデータ)をテーブルの列の値として保存する機能を持つものは少なくない. 本コースで使用しているPostgreSQLの場合はbyteaという列の型があり, これを用いてファイルの内容をデータベースのテーブルに詰め込むことは可能である.可能であると書いたのは,このような方法は限定的な目的でしか採用しないのが 一般的であるためである.なぜならデータベース内にテーブルの列として巨大なファイルを保存してしまうと, パフォーマンスの低下につながる場合があるためである.

3.はAmazon S3Microsoft Azure Storage, 国内のサービスであればさくらのクラウドConoHa VPSの オブジェクトストレージサービスのようなパブリックサービスを利用する方法である.こういったサービスは普通は有償だが, そのウェブアプリのデータ管理方法の1オプションとして提供するのは有用だろう.

本節では2.のウェブアプリを動作させているマシンのファイルシステムを直接利用する方法を学ぶことにする.

今回作成しているアプリケーションはウェブログということなので,「記事」に対して複数の「添付ファイル」を 関連付けて保存ができることにしよう.つまりデータモデルとしては_のようになる.

「添付ファイル」を追加したウェブログのためのデータモデル

今回はファイルの本体はデータベースではなくファイルシステム内の適当なフォルダーに保存するが, プログラム上は「添付ファイル」が_に示したようなデータモデルとして認識される必要があるので, やはり今までと同様に「添付ファイル」に相当するモデルクラスを定義する必要がある. このモデルクラスには,アップロードされたファイルの メタ情報 (ファイル名,ファイルタイプなど)だけを格納することにする.

各「添付ファイル」のレコードに対応する ファイルの在り処 の管理については,ASP.NET Core はそのための機能を提供しないので アプリケーションが自ら責任を持つ必要がある.今回は単純に事前定義した保存フォルダー内に,アップロードされたファイルを 「添付ファイル」の主キーと同じ名前で保存するという戦略をとることにしよう.1つのフォルダ内に配置されるファイルの名前は 衝突(重複)が許されないが主キーは重複する恐れがないためこのような方法が可能である.

それではまずは「添付ファイル」に対応するモデルクラスを作成しよう.この「添付ファイル」の情報には _に示す項目を含めることにしよう.

「添付ファイル」の情報
項目名必須/任意データ型備考
ID必須整数添付ファイルのID(主キー).
ファイル名必須文字列添付ファイルのファイル名.
コンテントタイプ必須文字列添付ファイルのファイルタイプ(MIME).
アップロード日時必須日付時刻添付ファイルをアップロードした日時.

この「添付ファイル」に対応するモデルクラスを定義しよう.クラス名はAttachment(あたっちめんと)とする. まずプロジェクト内の Models フォルダを右クリックし,「追加」→「クラス」をクリックする. 作成するクラス名を訊かれるのでAttachment(.csは省略可能)と入力して「追加」ボタンをクリックする. すると空のクラス定義が作られるので_の定義を書き込もう.

「添付ファイル」クラス
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.ComponentModel.DataAnnotations; // 追記

namespace T10b.Models
{
    public class Attachment
    {
        [Display(Name = "Id")]
        public int AttachmentId { get; set; }

        [Display(Name = "ファイル名")]
        public string Filename { get; set; } = "";

        [Display(Name = "コンテントタイプ")]
        public string ContentType { get; set; } = "";

        [Display(Name = "アップロード日時")]
        public DateTime Uploaded { get; set; }

        public int ArticleId { get; set; }    // 外部キー(ArticleのArticleIdを参照)
        public Article? Article { get; set; }  // 参照ナビゲーションプロパティ
    }
}

この参照ナビゲーションプロパティに合わせてArticleクラスにもコレクションナビゲーションプロパティを追加しておこう. Models/Article.cs に_に示す内容を追記する.

Articleクラスの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 「記事」クラス
public class Article
{
    [Display(Name = "記事ID")]
    public int ArticleId { get; set; }      // 記事ID

    [Display(Name = "タイトル")]
    public string Title { get; set; } = ""; // タイトル

    [Display(Name = "本文")]
    public string Body { get; set; } = "";  // 本文

    [Display(Name = "作成日時")]
    [DataType(DataType.DateTime)]
    public DateTime Created { get; set; }   // 作成日時

    [Display(Name = "更新日時")]
    [DataType(DataType.DateTime)]
    public DateTime Modified { get; set; }  // 更新日時

    [Display(Name = "公開状態")]
    public PublicationStateType PublicationState { get; set; } // 公開状態

    [Display(Name = "著者")]
    public string BlogUserId { get; set; } = ""; // 外部キー(BlogUserのIdを参照)

    public BlogUser? BlogUser { get; set; }      // 参照ナビゲーションプロパティ

    public List<Attachment>? Attachments { get; set; } // コレクションナビゲーションプロパティ
}

Attachmentクラスがデータベース側に反映されるようにするには,データコンテキストクラスにも手を加える必要がある. Data/T10bContext.cs に_に示す内容を追記しよう.

T10bContextクラスの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class T10bContext : IdentityDbContext<BlogUser>
{
    public T10bContext(DbContextOptions<T10bContext> contextOptions)
        : base(contextOptions)
    { }

    public DbSet<Article> Articles => Set<Article>();

    public DbSet<Attachment> Attachments => Set<Attachment>(); // ←追加
}

ここまで追記ができたらコマンドラインターミナル1_に示すコマンドを 用いてマイグレーションコードの生成とデータベース側への適用を行おう.

マイグレーション処理の生成と適用
PS> dotnet ef migrations add AddAttachment
PS> dotnet ef database update

アップロードされたファイルの保存場所については,前節と同様に決め打ちではなく設定ファイルで設定可能にしておこう. appsettings.json および SiteSettings.cs にそれぞれ__に示す内容を追記する.

appsettings.jsonの追記内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "T10bContext": "Host=localhost;Port=5432;Username=t10b_user;Database=t10b_db;Password=Xbi!YzNm5"
  },
  "SiteSettings": {
    "SiteName": "私のサイト",
    "SiteOwnerName": "名無しの権兵衛",
    "SiteContactAddress": "g-nanashi@thcu.ac.jp",
    "NumberOfArticlesEachPage": 5, // ←ここカンマを追記するの忘れないように!
    "UploadFileFolder": "Attachments" // 保存場所(絶対パスもしくは実行体からの相対パス)
  }
}
サイト設定のためのクラス
1
2
3
4
5
6
7
8
9
public class SiteSettings
{
    public string SiteName { get; set; } = "MySite";
    public string SiteOwnerName { get; set; } = "John Smith";
    public string SiteContactAddress { get; set; } = "john@example.com";
    public int NumberOfArticlesEachPage { get; set; }

    public string UploadFileFolder { get; set; } = ""; // 保存場所(絶対パスもしくは実行体からの相対パス)
}

「添付ファイル」の操作のためのアクションに関しては,専用のコントローラーを作成するのではなく, Articlesコントローラーに組み込むことにする.このために,添付ファイルのアップロード,取得,削除のための 3つのアクションUploadAttachmentGetAttachmentDeleteAttachmentを追加する. これらの画面イメージと画面遷移を_に示す. なお,この図では,図9b-3-1図9b-3-2で 提示済みの画面遷移の矢印は省略している.

「添付ファイル」関連のArticlessコントローラーの画面イメージ・画面遷移

ファイルの添付は記事編集画面(Edit)からのみ辿ることができるようにしておく.これは記事の新規作成時は 添付ファイルの作成が行えないことも意味するが,新規作成時にも添付ファイルを追加できるような設計にすること 自体はそれほど難しくない.ここでは簡単のためいったん保存済みに記事に対してしか添付ファイルの作成を行えないようにしている.

UploadAttachmentアクションはファイルのアップロードを担うアクションである.このアクションはフォームの表示と,フォームからの送信の 双方を行う必要があるためGET用とPOST用の両方を用意する必要がある. GetAttachmentは添付されたファイルを取得するためのアクションである.このアクションは基本的にGET用のみであるが,ビューは持たず ファイルの内容そのものをレスポンスとしてクライアントに返送する. このアクションに限り匿名ユーザー,つまりは一般の閲覧者にもアクセス可能にしておく. DeleteAttachmentは添付ファイルの削除のためのアクションである. また,これらに合わせて記事の個別表示画面(Details)および記事編集画面(Edit)にも添付ファイルの一覧が表示されるようにしておく.

アプリケーションでファイルをアップロードするには以下のようにする.

  1. IFormFileインターフェース型のプロパティをもつモデルクラスを定義する.
  2. ↑のモデルクラスを @model ディレクティブに指定したビューを作成する.
  3. form 要素の enctype 属性に multipart/form-dataを指定する.
  4. フォームの input 要素に asp-for タグヘルパーを使って↑のプロパティを関連付ける.

まず1.のためのモデルクラスを用意しよう.プロジェクト内の Models フォルダを右クリックし,「追加」→「クラス」をクリックする. 作成するクラス名を訊かれるのでUploadFile(.csは省略可能)と入力して「追加」ボタンをクリックする. すると空のクラス定義が作られるので_の定義を書き込もう.

ファイルアップロードのためのモデルクラス
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
using System.ComponentModel.DataAnnotations; // 追記

namespace T10b.Models
{
    public class UploadFile
    {
        [Display(Name = "ファイル")]
        public IFormFile File { get; set; } = null!;
    }
}

つぎにUploadAttachmentアクションとそのためのビューを作成しよう. このために以降の処理で必要となるいくつかの小道具を追加しておこう. Articlesコントローラーに_に示す内容を追記しよう. ファイルの冒頭に_に示すusingディレクティブを追記する必要がある点に注意しよう.

必要なusingディレクティブ
1
using System.Reflection;
Articlesコントローラーの追記内容(ユーティリティメソッド・プロパティ)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// アップロードされたファイルを保存するフォルダーのパスを返すプロパティ
private string UploadFileFolder 
{
    get 
    {
        var path = _siteSettings.UploadFileFolder;          // 設定ファイルから
                                                            // 保存フォルダーのパスを取得する.
                                                                    
        if (!Path.IsPathFullyQualified(path))               // ↑が絶対パスで指定されていない場合:
        {                                                   //
            var execDir = Directory.GetParent(              //    プログラムの実行体(.exe)がある場所の
                Assembly.GetExecutingAssembly().Location)!; //    フォルダー情報を得て,
                                                            // 
            path = Path.Combine(execDir.FullName, path);    //    そのフォルダーからの相対パスとして解釈する.
        }                                                   //

        if (!Directory.Exists(path))                        // 指定された保存フォルダーがまだ存在しない場合:
            Directory.CreateDirectory(path);                //    そのフォルダーを作成する.

        return path;                                        // 保存フォルダーのパスを返す.
    }
}

// 指定された「添付ファイル」を保存するパスを取得するためのヘルパーメソッド
// (ファイルの保存先のファイルパス((保存フォルダー)\(添付ファイルID))を取得する).
private string GetAttachmentFilePath(Attachment attachment)
    => Path.Combine(UploadFileFolder, attachment.AttachmentId.ToString());

_では private なUploadFileFolderプロパティ(読み取り専用)と, GetAttachmentFilePath()メソッドを定義している.

UploadFileFolderプロパティは設定ファイルから,アップロードされたファイルの保存先のフォルダの場所を 取得して,絶対パスに変換して返すプロパティである.これは例えば以下のように解釈される.

例) appsettings.json のUploadFileFolderの設定値
"Attachments"
例) プログラムの実行体の場所
"C:\Users\Hoge\source\repo\WebApp\bin\Release\WebApp.dll"
添付ファイルの保存先
"C:\Users\Hoge\source\repo\WebApp\bin\Release\Attachments\"

また,このプロパティはこの保存フォルダが存在していなければ作成する,という作用も持っている.

GetAttachmentFilePath()メソッドは,引き数として渡された_Attachmentクラスのインスタンスに対応する ファイルパスを取得するメソッドである.このパスはコメントに書いてあるように保存フォルダー\添付ファイルID という形式のパスとなる.これは例えば以下のような解釈を行い,メソッドの戻り値としては 拡張子なしのファイルパス を返してくる.

例) 保存フォルダー(=前述の「添付ファイルの保存先」)
"C:\Users\Hoge\source\repo\WebApp\bin\Release\Attachments\"
例) 添付ファイルID
72
GetAttachmentFilePath()メソッドの戻り値
"C:\Users\Hoge\source\repo\WebApp\bin\Release\Attachments\72"

  1. タブのタイトルは「開発用PowerShell」もしくは「Developer PowerShell」となっている. ↩︎

Last updated on 2024-06-18
Published on 2024-06-18

Powered by Hugo. Theme by TechDoc. Designed by Thingsym.