🌟

TEI/XMLファイルをS3互換のオブジェクトストレージでホストする

に公開

概要

TEI/XMLファイルをS3互換のオブジェクトストレージでホストする機会がありましたので、備忘録です。具体的には、mdx Iのオブジェクトストレージを対象にします。

https://mdx.jp/mdx1/p/about/system

背景

TEI/XMLファイルを読み込み、その内容を可視化するウェブアプリケーション(Next.js)を構築します。この時、ファイル数やサイズが小さい場合は、publicフォルダに格納していましたが、これらが大きくなった場合、別の場所でホストすることを考えました。

場所の選択肢は多々ありますが、今回はS3互換であるmdx Iのオブジェクトストレージを対象にします。

GUIを用いたオブジェクトストレージへのファイルアップロード

オブジェクトストレージへTEI/XMLファイルをGUI経由でアップロードする方法も多々あります。その中で、これまではCyberduckを使用する方法や、GakunNin RDMを使用する方法などを紹介しました。

https://zenn.dev/nakamura196/articles/fa9478f5c0376c

https://zenn.dev/nakamura196/articles/b1f4f73136fb5f

一方、今回の事例では、TEI/XML以外のコンテンツをDrupalで管理していました。そこで、Drupalとオブジェクトストレージを接続し、ユーザはDrupalの操作で完結できるようにしました。

Drupalとオブジェクトストレージの接続

以下のモジュールを使用します。

https://www.drupal.org/project/s3fs

インストール後、環境設定のページ/admin/configから、S3 File Systemを選択します。

そして、アクセスキーや秘密鍵を登録し、さらにS3のバケット名を登録します。

またAdvanced Configuration OptionsのCustom Host Settingsにおいて、https://s3ds.mdx.jpを入力します。

これでオブジェクトストレージとの接続設定は完了です。

その後、各コンテンツタイプのフィード設定において、アップロード先として「S3 File System」を選択します。

また、今回はTEI/XMLファイルがアップロード対象となるため、「許可されている拡張子」として、xmlを入力します。

この結果、DrupalのGUIを介してアップロードしたTEI/XMLファイルが、mdx Iのオブジェクトストレージに格納されるようになりました。

(参考)DrupalのJSON:APIを用いたファイルの一括アップロード

TEI/XMLの初期登録にあたり、Pythonを用いた一括登録を行いました。JSON:APIを用いたファイルの一括アップロードの方法は、以下の記事などが参考になりました。

https://www.drupal.org/node/3024331

一例ですが、以下のようなスクリプトで実現できました。

import requests
import json
import os
from dotenv import load_dotenv
from glob import glob
from tqdm import tqdm

class ApiClient:
    def __init__(self):
        load_dotenv(override=True)

        # DrupalサイトのURL(例)
        self.DRUPAL_BASE_URL = os.getenv("DRUPAL_BASE_URL")  

        # エンドポイント(JSON:API)
        # self.JSONAPI_ENDPOINT = f"{self.DRUPAL_BASE_URL}/jsonapi/node/article"

        # 認証情報(Basic認証)
        self.USERNAME = os.getenv("DRUPAL_USERNAME")
        self.PASSWORD = os.getenv("DRUPAL_PASSWORD")

    def login(self):
        # ログインリクエスト
        login_url = f"{self.DRUPAL_BASE_URL}/user/login?_format=json"

        login_response = requests.post(
            login_url,
            json={"name": self.USERNAME, "pass": self.PASSWORD},
            headers={"Content-Type": "application/json"}
        )

        if login_response.status_code == 200:
            self.session_cookies = login_response.cookies

    def get_csrf_token(self):
        # CSRFトークンを取得
        csrf_token_response = requests.get(
            f"{self.DRUPAL_BASE_URL}/session/token",
            cookies=self.session_cookies  # ここでログインセッションを渡す
        )

        if csrf_token_response.status_code == 200:
            # return csrf_token_response.text
            # self.csrf_token = csrf_token_response.text
            self.headers = {
                "Content-Type": "application/vnd.api+json",
                "Accept": "application/vnd.api+json",
                "X-CSRF-Token": csrf_token_response.text,
            }
        else:
            # raise Exception(f"CSRFトークン取得失敗: {csrf_token_response.status_code} {csrf_token_response.text}")
            self.csrf_token = None

    def upload_file(self, type, uuid, field, file_path, verbose=False):
        url = f"{self.DRUPAL_BASE_URL}/jsonapi/node/{type}/{uuid}/{field}"

        # ファイル名を取得
        filename = os.path.basename(file_path)

        # ファイルをバイナリモードで読み込む
        with open(file_path, 'rb') as f:
            file_data = f.read()

        headers = self.headers.copy()
        headers['Content-Type'] = 'application/octet-stream'
        headers['Content-Disposition'] = f'attachment; filename="{filename}"'

        # ファイルをアップロード
        response = requests.post(url, headers=headers, cookies=self.session_cookies, data=file_data)

        if response.status_code == 200:
            if verbose:
                print(f"ファイルアップロード成功: {filename}")
        else:
            print(f"ファイルアップロード失敗: {response.status_code} {response.text}")

すでに対象コンテンツが作成済みで、例えばfield_fileといったフィールドにファイルをアップロードする目的で使用することができます。

より適切な方法があるかと思いますが、以下のように使用できます。

client = ApiClient()
client.login()
client.get_csrf_token()

uuid = "cefa8076-4ddf-4c05-a03d-fcdebbf0c209"
file = "<ファイルのパス>"
content_type = "<コンテンツタイプ>"
field = "field_file"

client.upload_file(content_type, uuid, field, file)

なお、以下のような環境変数が必要です。

.env
DRUPAL_BASE_URL=
DRUPAL_USERNAME=
DRUPAL_PASSWORD=

Next.jsからの利用

オブジェクトストレージにアップロードされたTEI/XMLファイルを、Next.jsなどのアプリケーションから読み込みます。

以下のライブラリを使用することができました。

https://www.npmjs.com/package/@aws-sdk/client-s3

具体的には、以下のような形で利用できました。

import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { DOMParser, Document as XMLDocument } from '@xmldom/xmldom';

export const convertToXml = (xmlText: string): XMLDocument => {
  const parser = new DOMParser();
  const xml = parser.parseFromString(xmlText, 'text/xml');
  return xml;
};

export const getXml = async (id: string): Promise<XMLDocument | null> => {
  const client = new S3Client({
    region: 'us-east-1', // サービスによって設定したりしなかったり
    endpoint: process.env.S3_ENDPOINT || '',
    credentials: {
      accessKeyId: process.env.S3_ACCESS_KEY_ID || '',
      secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '',
    },
  });

  const command = new GetObjectCommand({
    Bucket: process.env.S3_BUCKET || '',
    Key: `xml/${id}.xml`,
  });

  const response = await client.send(command);

  const content = await response.Body?.transformToString();

  if (!content) {
    return null;
  }

  return convertToXml(content);
};

以下のような環境変数とともに使用します。

.env
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_ENDPOINT=https://s3ds.mdx.jp
S3_BUCKET=

結果、以下のような構成を実現することができます。

今後の展望: LEAF-Writerとの接続

今回はDrupal(実際には、mdx Iのオブジェクトストレージ)にアップロードされたTEI/XMLファイルの編集環境は用意していませんが、例えば以下のLEAF-WriterのDrupalモジュールを使用することで、TEI/XMLファイルの編集と管理をCMS上で完結させられる可能性があります。

https://gitlab.com/calincs/cwrc/leaf-writer/leaf_writer

また、GakuNin RDMとLEAF-Writerを接続したプロトタイプとして、以下も参考になりましたら幸いです。

https://zenn.dev/nakamura196/articles/e90f671b0565d2

まとめ

今回、TEI/XMLファイルをS3互換のオブジェクトストレージでホストする一例について紹介しました。このような方法を採用する利点や欠点が考えられるため、用途に応じた構成を考える上で、本記事が参考になりましたら幸いです。

Discussion