見出し画像

[Drogon]JavaScriptとの連携とファイルアップロード


さて今回は、ファイルのアップローダを作っていきたいと思います。

Drogonのexampleには、ファイルアップロードのサンプルもあるのですが、
これはルーティングなどのコードがmain.ccにべた書きされており、初学者が実用に向けて応用するには少しハードルが高いです。

そこで、実際にDrogonのビューとjavascript、そしてコントローラを連携させて、ファイルのアップロードを受け付けるサンプルを解説したいと思います。

プロジェクトとか

作成の仕方は以前の記事をご覧ください。
今回からプロジェクトやコントローラは作る内容を一覧化していきます。

  • プロジェクト
    名称:javascript_cooperation

  • コントローラ
    種類:HTTPController
    名称:uploadSample

  • CSPファイル
    名称:sampleView.csp

こんな感じの名称でプロジェクトとコントローラをdg_ctlコマンドで作成し、viewsの下にCSPファイルを配置していきます。

CSPファイルの配置

今回はコントローラとデータをやり取りするようなコードは特にありませんので、cspと言いつつhtmlファイルですね。

  • sampleView.csp

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>File upload</title>
</head>
<body>
<form name="uploadForm">
    <input type="file" id="chooseFile" />
    <input type="button" id="uploadButton" value="アップロード" />
    <span id="progressInformation" ></span>
    <progress id="progressOfUpload" max="100" />
</body>
</form>
<script type="text/javascript" src="/js/fileupload.js"></script>
</html>

javascriptファイルを配置する

以前の記事で解説したCSSを使う際の手順と同様、buildディレクトリにjsディレクトリを作成し、そこにファイルアップロード用のjavascriptファイルを作成します。

私が作ったのはこんな感じ

  • fileupload.js


const progressBar = document.getElementById('progressOfUpload');
const button = document.getElementById('uploadButton');
const progressInformation = document.getElementById("progressInformation");
const xhr = new XMLHttpRequest();

button.addEventListener('click', executeUpload);

xhr.onload=function(evt){
        
    alert(xhr.responseText);
}

xhr.onerror=function(){
    alert('アップロード失敗しました...\n');
}

xhr.upload.onprogress=function(evt){
    let progVal = (evt.loaded / evt.total * 100).toFixed(1);
    progressBar.value = progVal;
    progressInformation.innerHTML = progVal + '%';
}

function executeUpload(evt)
{
    const chosenFile = document.getElementById('chooseFile').files[0];
    let formData = new FormData();
    formData.append( "file", chosenFile );
   
    xhr.open('POST', 'upload', true);
    xhr.send(formData);
};

まあ、正直なところ個人的にはあまりjavascriptは好きではないです(というか大嫌いです)し、ユーザ多い言語ですからこれは解説省きます。

ファイルコピペしてください。

コントローラの作成

次に、コントローラのuploadSample.hを加工します。

  • uploadSample.h

#pragma once

#include <drogon/HttpController.h>

using namespace drogon;

class uploadSample : public drogon::HttpController<uploadSample>
{
    public:
    METHOD_LIST_BEGIN
    METHOD_ADD(uploadSample::showMainpage, "/", Get);
    METHOD_ADD(uploadSample::recieveUpload, "/upload", Post);
    METHOD_LIST_END

    private:
    std::vector<std::string> enabledExtension;
    bool isEnabledExtension(const std::string_view) const noexcept;
    std::string createDirectoryName(const std::string &) const noexcept;

    public:
    uploadSample():
    enabledExtension({"png", "jpg", "bmp", "svg"})
    {}

    void showMainpage(const HttpRequestPtr &,
            std::function<void(const HttpResponsePtr &)> &&callback) const;

    void recieveUpload(const HttpRequestPtr &,
            std::function<void(const HttpResponsePtr &)> &&callback) const;
};

まぁまぁいじりました。

ポイントとしては、

  • ・METHOD_ADDにPOSTを追加
    動きの機序をクライアント始点で考えると、

    1. sampleView.cspを表示(Javascriptの在処書いてる)

    2. javascriptをロード(アップロードの手順が書いてる)

    3. javascriptを実行してファイルをPOST(アップロードの手順を実行する

このような手順を経てAjax(非同期javascriptとXML)を使って、ファイルの中身をPOSTさせるのです。そのため当然Drogonサーバ側にはPostリクエストへの受け口が必要となります。

  • 受け付けるファイル形式はしっかり決める
    必須ではないですが、セキュリティ的にも意図しないファイルは受け取らないのが吉です。今回は画像ファイルを数種類受け取れるように決めておきます。

こんなところでしょうか。

そしてuploadSample.ccにコードを記載します。

  • uploadSample.cc

#include "uploadSample.h"
#include <iomanip>

void uploadSample::showMainpage(const HttpRequestPtr &req,
        std::function<void(const HttpResponsePtr &)> &&callback) const
{
    callback( drogon::HttpResponse::newHttpViewResponse("sampleView.csp"));
}

void uploadSample::recieveUpload(const HttpRequestPtr &req,
        std::function<void(const HttpResponsePtr &)> &&callback) const
{
    drogon::MultiPartParser fileParser;
    
    auto resp = drogon::HttpResponse::newHttpResponse();

    if (fileParser.parse(req) != 0 || fileParser.getFiles().size() != 1)
    {
        resp->setBody("何かファイルを選んでください!");
        resp->setStatusCode(k403Forbidden);
        callback(resp);
        return;
    }
    
    auto &file = fileParser.getFiles()[0];
    if( isEnabledExtension(file.getFileExtension()) )
    {
        auto uploadDir = 
            createDirectoryName(
                drogon::app().getUploadPath()
            );
        file.save(uploadDir);
        resp->setBody("ファイルを受信して保存しました。");
        resp->setStatusCode(k200OK);
    }
    else 
    {
        resp->setBody("ふざけんじゃねぇぞ、ウィルスでも送る気だったか?");
        resp->setStatusCode(k415UnsupportedMediaType);
    }

    callback(resp);
}

bool uploadSample::isEnabledExtension(const std::string_view target) const noexcept
{
    auto result = false;
    for(auto enabled : enabledExtension)
    {
        result |= (target == enabled);
    }
    return result;
}

std::string uploadSample::createDirectoryName(const std::string &uploadRoot) const noexcept
{
    auto now = std::time( nullptr );
    auto localNow = *std::localtime( &now );
    std::ostringstream oss;
    oss << std::put_time( &localNow, "%Y_%m_%d" );
    return uploadRoot + "/" + oss.str(); 
}

HTTPで渡ってきたデータをファイルとして復元するためにはdrogon::MultiPartParserを使っています。

drogon::MultiPartParser fileParser;

これはparseメソッドによって、リクエストのデータストリームからファイルやパラメータを取り出します。
parseメソッドを実行後、getFilesでファイルを操作するためのオブジェクトを取り出すことができます。こんなかんじで、

if (fileParser.parse(req) != 0 || fileParser.getFiles().size() != 1)

リクエスト飛ばしたくせに何も送ってこないとは何事だ!という確認もできます。

fileの情報を取り出すには、以下のようにします。

auto &file = fileParser.getFiles()[0];

ここで取り出されるのは、HTTPリクエストでアップロードされたファイルを操作するための、drogon::HttpFileというオブジェクトです。
このオブジェクトはファイルそのものの情報も細かくとることができます。
今回は先述の拡張子からファイルの種類を確認するためにgetFileExtension() を使用しています。

if( isEnabledExtension(file.getFileExtension()) )

ファイルが保存される先は、drogon::app().getUploadPath() メソッドによって取ることができます。
config.jsonで設定することもできますが、今回はデフォルトの [プロジェクトルート]/build/uploads ディレクトリです。

ただ色々アップロードして遊ぶにしても、大量にファイルが入ってごちゃごちゃするのは困りますので、日付と時間でパスを作りそこにファイル保存しています。

    auto uploadDir = 
        createDirectoryName(
            drogon::app().getUploadPath()
        );
    file.save(uploadDir);

HttpFileオブジェクトの save メソッドは、引数を与えなければdrogon::app().getUploadPath() で取得できるのと同じパスにファイルを保存します。

また、パスを与えれば今回のように任意のパスにファイルを保存することができます。

ビルドと実行

今回もビルドディレクトリに入ってビルドと実行していきましょう。

~/javascript_cooperation/build$ cmake .. & make
~/javascript_cooperation/build$ ./javascript_cooperation

ブラウザを使用して、localhost:80/uploadSample/にアクセスしてみましょう。

味気ないアップロード画面

ファイルを選択して、アップロードボタンをポチっとな。

どうやら成功したようだ

ファイルのアップロードが終わったら、ターミナルでuploadの下を確認してみましょう。

~/javascript_cooperation/build$ ls uploads/2022_06_16
名称未設定.png

このようにアップロードされたファイルが保存されていれば成功です。

さいごに

今回はjavascriptとの連携、そしてファイルのアップロードの仕方について解説しました。

もうこのくらい色々開設すればそろそろ何か面白いアプリの一つでも作れそうですね。

私は発明家というよりは技術屋の気質が非常に強く、技術は学んでもそれを使うところが思いつかない性格なので、Drogonを利用してどんな面白いことができるかは皆様にお任せいたします。

いいなと思ったら応援しよう!