herokuで動くDjangoのmarkdownxの画像アップロード先をcloudinaryにした話


あらすじ

副業案件でDjango製のブログサイトの開発をサポートしていたときのお話です。

ブログ記事はmarkdownで書きたいということで、django-markdownxを導入していました。
開発は難なく完了。
herokuにデプロイします。

なんと、画像がアップロードできません

本来であれば、テキストエリアに画像をドロップすると、markdownxのjsによってアップロードされ、画像のパスがmarkdown形式でテキストエリアに表示されます。

本番環境で試してみると、画像をドロップ後、数秒たたないうちに画像のパスがテキストエリアに表示されましたが、プレビューに画像が表示されません。
コンソールを見ると、画像に対して404になっていました。
herokuにアクセスし、アプリケーションのディレクトリを確認すると、アップロード先に指定しているディレクトリに画像がありませんでした。

問題発覚

各所でも言及されていますが、herokuではDEBUG=FalseのDjangoはファイルをアップロードできません。
DEBUG=Trueにして運用するわけにもいかないので先方と相談した結果、heroku+Djangoで割と実績のあるcloudinaryを画像アップロード先とすることにしました。

cloudinaryのSDKをプロジェクトにインストールし、いざ。
しかし、今までと同様、テキストエリアに画像パスが表示されるものの、画像は表示されませんでした。

cloudinaryを直接確認してみると、アップロードしたファイルが存在していました。
ところが、ファイルのURLがテキストエリアに表示されたURLと異なるのです。

原因

原因は、markdownxとcloudinaryが親切だからでした。

markdownxさんは画像をアップロードするとき、既存のファイルとファイル名が衝突しないように、ファイル名をuuidに変更したうえでアップロードします。

一方、cloudinaryさんもまた、既存のファイルとファイル名が衝突しないように、ファイル名をuuidに変更したうえで保存します。

これらを組み合わせると、markdownxさんは名前を変えた上でcloudinaryさんに渡した後、cloudinaryさんがどんな名前にしてファイルを保存したのかを聞かず、自分が知っているファイル名をブラウザに返します。
この動きによって、cloudinaryにあるファイル名とテキストエリアに表示されるURLが異なっていました。

解決策

markdownxさんには辞めてもらって、cloudinaryさんの独り言に耳を傾けましょう。

markdownxは、デフォルトでは画像アップロードをライブラリ内のビューで処理します。
設定を変更して、自前のビューに転送するよう変更します。

settings.py
+ MARKDOWNX_UPLOAD_URLS_PATH = '/upload'

画像アップロード処理用ビューを用意します。
markdownxの画像アップロードリクエストは、'image'という名前でファイルがきます。
レスポンスは{"image_code": テキストエリアに表示する画像パス}形式のJSONです。
cloudinaryのアップロードのレスポンスは辞書型で、'url'で画像URLを取得できます。

urls.py
+ path('upload', views.UploadView.as_view(),name='upload'),
views.py
# 追記
import cloudinary
import cloudinary.uploader
import cloudinary.api
from django.conf import settings
from django.http.response import JsonResponse

class UploadView(View):
    def post(self,request, *args,**kwargs):
        file = request.FILES['image']

        cloudinary.config( 
            cloud_name = settings.CLOUDINARY_STORAGE['CLOUD_NAME'], 
            api_key = settings.CLOUDINARY_STORAGE['API_KEY'], 
            api_secret = settings.CLOUDINARY_STORAGE['API_SECRET']
        )
        res = cloudinary.uploader.upload(
            file = file,
            folder = settings.MARKDOWNX_MEDIA_PATH,
        )

        return JsonResponse({"image_code": f"![]({res['url']})"})

これで、markdownxを介さずにcloudinaryに直接アップロードし、正しいURLをテキストエリアで取得できるようになりました。

heroku + django + cloudinaryという組み合わせはメジャーそうなのに、markdownが絡んだ記事がなくて苦労しました。
誰かのお役に立てると幸いです。

みなさんも人の話には耳を傾けましょう。