【C#】【ASP.NET Core】画像データベースを作ってみる

TL;DR;

タイトルのとおりなんですけど、ASP .NET Coreで画像をアップロードしたりダウンロードしたりするコントローラを作成しましょうってやつです。ついでにデータベースにも登録します。

環境

記事を執筆したタイミングでの環境は以下のとおりです

  • Ubuntu 20.04
  • .NET Core 3.1.403
  • MySQL 8.0.23

データベースの構築

冒頭でも述べたように、アップロードした画像をデータベースに登録できるようにします。ただし、画像のバイナリそのものをデータベースに格納するのではなく、アップロードされた画像はファイルとして残し、そのパスをデータベースに登録する方式を取ります。

ユーザ/テーブルの作成

やってくれ。

テーブルの作成

テーブルには、画像のID(media_id)と画像のパス(media_path)があれば最低限事足りると思います。あとはユースケースに合わせて例えば画像の作成者(media_owner)などを適宜追加すると良いでしょう。また、今回は画像のIDは重複してほしくないのでPRIMARY KEY(UNIQUEでもいいけど)にし、採番はMySQLに任せてしまっても不都合はないと思うのでAUTO_INCREMENTにしています。

CREATE TABLE media(media_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, media_path CHAR(64) NOT NULL); 

実装

てことで実装していきます。ちなみにプロジェクト作成に使用したテンプレートはWeb/WebAPI(ショートネームはwebapi)です。

POSTコントローラの実装

まずはどのような形式で画像を受け取るのか考えていきます。最低限として画像のデータが必須となります。画像の圧縮フォーマットもパラメータとして受け取ったほうがいい気がしますが、PNGしか扱いませんみたいなシステムにするのであれば必要ないと思います。あと、画像サイズをパラメータに含めたいところですが、画像のデータをデコードするときに画像サイズは副次的に計算できるため、これはあってもなくても良いでしょう。クライアント側から見たら無いほうが便利に見えるかもしれません。

てことでいろいろ挙げてみましたが、今回はパラメータとして画像データと画像フォーマットを扱うことにします。また、これらのデータはリクエストボディに含めて送信することにします。まぁまずは実装を見てくれ。

using System;
using System.Collections.Generic;
using System.Linq;
using MySql.Data.MySqlClient;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;



[HttpPost]
public async Task<IActionResult> Post(){
    // リクエストボディを文字列として読み込む
    // このときはまだ{key}={value}
    string body;
    using (var reader = new StreamReader(HttpContext.Request.Body)){
        body = await reader.ReadToEndAsync();
    }

    // 受け取ったリクエストボディをパースするよ
    // {key}={value}が望ましいけど{key}={空っぽ}とか{key}={base64}とかも対処するよ
    var queryPair = body.Split("&");
    var query = new List<KeyValuePair<string, string>>();
    foreach(var q in queryPair){
        var tmp = q.Split("=", 2);
        if(tmp.Count() == 2){
            query.Add(new KeyValuePair<string, string>(tmp[0], tmp[1]));
        }
    }


    var receivedData = query.Where(x => x.Key == "data");
    String data;
    if(receivedData.Count() == 1){
        data = receivedData.First().Value;
    }else{
        return BadRequest(new {reason = "nothing or multiple data is provided."});
    }

    var receivedType = query.Where(x => x.Key == "type");
    String type;
    if(receivedType.Count() == 1){
        type = receivedType.First().Value;
    }else{
        return BadRequest(new {reason = "nothing or multiple media type is provided"});
    }

    // 拡張子を設定するよ〜〜〜〜
    System.Drawing.IImaging.ImageFormat format;
    switch (type.ToLower()){
        case "png":
            format = System.Drawing.Imaging.ImageFormat.Png;
            break;
        case "jpg":
        case "jpeg":
            format = System.Drawing.Imaging.ImageFormat.Jpeg;
            break;
        case "gif":
            format = System.Drawing.Imaging.ImageFormat.Gif;
            break;
        default:
            return BadRequest(new {reason = "unsupported media type"});
    }

    // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}.{拡張子}
    var filename = $"{Guid.NewGuid().ToString("D")}.{format.ToString().ToLower()}";


    // 文字列として受け取った画像ファイルをbyte列に変換
    var byteData = new List<byte>();
    for(var i = 0;i < data.Length;i+=2){
        byteData.Add(Convert.ToByte(data.Substring(i, 2), 16));
    }

    // GUIDの衝突がめちゃくちゃ稀に、ほんと稀に存在するかもしれないくらいのアレなので念の為削除
    if(System.IO.File.Exists($"media/{filename}")){
        System.IO.File.Delete($"media/{filename}");
    }

    // 保存
    using (var image = System.Drawing.Image.FromStream(new MemoryStream(byteData.ToArray()))){
        image.Save($"media/{filename}", format);
    }

    ulong mediaId = 0;
    using (var connection = new MySqlConnection("server=<MySQLのホスト。localhostとか>;userid=<MySQLに接続するユーザ>;password=<MySQLのパスワード>;database=<使用するデータベース>;")){
        {
            // ファイルを登録するよ
            var cmd = new MySqlCommand($"insert into media(media_path) values ('{filename}')", connection);
            cmd.Connection.Open();
            cmd.ExecuteNonQuery();
            cmd.Connection.Close();
        }

        {
            // 昇順で自動採番されるのでもう一度接続して最大値を取り出すよ
            var cmd = new MySqlCommand($"select max(media_id) as max from media", connection);
            cmd.Connection.Open();
            var reader = cmd.ExecuteReader();
            while(reader.Read()){
                mediaId = ulong.Parse(reader["max"].ToString());
            }
            cmd.Connection.Close();

        }
    }

    return Ok(new {media_id = mediaId});
}

パーツごとに解説していきます。まずは15行目~46行目のこの部分

    string body;
    using (var reader = new StreamReader(HttpContext.Request.Body)){
        body = await reader.ReadToEndAsync();
    }

    // 受け取ったリクエストボディをパースするよ
    // {key}={value}が望ましいけど{key}={空っぽ}とか{key}={base64}とかも対処するよ
    var queryPair = body.Split("&");
    var query = new List<KeyValuePair<string, string>>();
    foreach(var q in queryPair){
        var tmp = q.Split("=", 2);
        if(tmp.Count() == 2){
            query.Add(new KeyValuePair<string, string>(tmp[0], tmp[1]));
        }
    }


    var receivedData = query.Where(x => x.Key == "data");
    String data;
    if(receivedData.Count() == 1){
        data = receivedData.First().Value;
    }else{
        return BadRequest(new {reason = "nothing or multiple data is provided."});
    }

    var receivedType = query.Where(x => x.Key == "type");
    String type;
    if(receivedType.Count() == 1){
        type = receivedType.First().Value;
    }else{
        return BadRequest(new {reason = "nothing or multiple media type is provided"});
    }

ここではリクエストボディに含まれているデータをパースしたりしてます。そのうえで、さっき必須パラメータとしてあげた画像データ(data)や画像フォーマット(type)が存在するか調べて、存在しなかったり複数回定義されてたらとりあえずBadRequestとしてレスポンスを返してます。

それでもって70~73行目のこの処理

// 文字列として受け取った画像ファイルをbyte列に変換
var byteData = new List<byte>();
for(var i = 0;i < data.Length;i+=2){
    byteData.Add(Convert.ToByte(data.Substring(i, 2), 16));
}

やってることは単純で,受け取った画像データは例えば302A421FCC59........みたいな感じになってるからこれを2文字ずつ取り出してあげればバイト列が復元されるよねってお話.ちなみに復元されたバイト列の数を見ればそれがそのままファイルサイズになる.

それで最後にこれ

    ulong id;
    using (var connection = new MySqlConnection("server=<MySQLのホスト。localhostとか>;userid=<MySQLに接続するユーザ>;password=<MySQLのパスワード>;database=<使用するデータベース>;")){
        {
            // ファイルを登録するよ
            var cmd = new MySqlCommand($"insert into media(media_path) values ('{filename}')", connection);
            cmd.Connection.Open();
            cmd.ExecuteNonQuery();
            cmd.Connection.Close();
        }

        {
            // 昇順で自動採番されるのでもう一度接続して最大値を取り出すよ
            var cmd = new MySqlCommand($"select max(media_id) as max from media", connection);
            cmd.Connection.Open();
            var reader = cmd.ExecuteReader();
            while(reader.Read()){
                id = ulong.Parse(reader["max"].ToString());
            }
            cmd.Connection.Close();

        }
    }

    return Ok(new {media_id = id});

SQL文を見てもらえればなにやってるのかはだいたいわかると思うんですが、INSERTでデータを投げつけてから今度はSELECTidの最大値を取得してるだけです。その値が今回投げつけた画像ファイルのIDとして扱うことが出来ます。結果として{'media_id':2222}みたいなレスポンスがJSONで返ってきます。

GETコントローラの実装

次に、画像を照会するためのGETコントローラを作成していきます。パラメータはmedia_idだけあれば十分でしょう。ということで早速実装を見ていきましょう。

[HttpGet]
public  ActionResult Get(){
  var media_id = long.Parse(HttpContext.Request.Query["media_id"].ToString());
  var media_path = "";

  using (var connection = new MySqlConnection("server=<MySQLのホスト。localhostとか>;userid=<MySQLに接続するユーザ>;password=<MySQLのパスワード>;database=<使用するデータベース>;")){
    var cmd = new MySqlCommand($"select media_path from media where media_id = {media_id}", connection);

    cmd.Connection.Open();
    var reader = cmd.ExecuteReader();

    if(reader.HasRows == 0){
      return NotFound(new {reason = "specified media id does not exist"});
    }

    while(reader.Read()){
      media_path = reader["media_path"].ToString();
    }

    cmd.Connection.Close();
  }

  var file = new BinaryReader(new StreamReader($"media/{media_path}").BaseStream);
  var stream = new MemoryStream(file.ReadBytes((int)file.BaseStream.Length));

  return File(stream, $"image/{Path.GetExtension(media_path).Trim('.')}");
}

説明、いる?

DELETEコントローラの実装

そのうちやる

おすすめ

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です