WEBアプリテンプレート(管理機能)

前回紹介したdjangoWEBアプリテンプレートに関して、個別の機能について気が向いたらメモを残しておきます。

ユーザー(www-data)の設定

djahgoのサービスはwww-dataというユーザー(debian-slimイメージに最初から居る)で起動するので、/code以下のファイル所有権もwww-dataとし、また実際のアプリ開発のために起動したコンテナにVScodeにアタッチして編集したりgitでソース管理出来るようにwww-dataユーザーでログインすることとし、念のためsudo権限を与えておく事とした。

# Dockerfile
RUN apt update; apt install -y git sudo
......
# usermod for www-data & sudo group
RUN usermod -d /code www-data; \
    usermod -s /bin/bash www-data; \
    echo "%www-data ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/www-data

# docker-compose.yml
services:
  django:
    build: .
    image: django_image:test
    user: www-data
    command: /code/startup
.......

こうすることで/code/startupはwww-dataの権限で実行されてテンプレートの構築が行われる。ただし、コンテナでマウントしたディレクトリの扱いがMacとそれ以外では異なるようで、Linuxなどの場合コンテナとホストのUIDが一致していない場合、ホストのユーザーで作成した/codeにwww-data権限で書込が出来ない問題がある。ここでは/code/startupの初期の段階で/codeの所有権をコンテナのwww-dataに取得させている(その為にパスワード無しのsudo権限を与えてしまった・・・・)

adminユーザーの設定

コンテナ側のユーザー管理とは別に、django WEBアプリにも認証があるため、少なくとも管理ユーザーの作成が必要(認証する気がなければ無くても良いかも知れない)。これもサイト構築毎に毎回は面倒なので、/code/startupのテンプレート作成時に自動生成させている。

ユーザーデータベースの更新は本来インタラクティブモードで動作するが、全自動で行うために下記のようにshellでコマンドを食わせている。

# startup
python manage.py migrate # データベースの初期化

cat <<CREATESUPER | sed -r "s/^\s*//g" | python manage.py shell
    from django.contrib.auth import get_user_model
    User = get_user_model()
    User.objects.create_superuser('admin' ,'', 'admin')
CREATESUPER

本テンプレートでは認証画面への遷移や管理画面へのリンクはnavbarに配置しており、テンプレートのcommon.tsでDOM構築している。

テンプレートのDOM構造は先日投稿のものをTypescript対応にしたもので、少し冗長になる感じだけど上手く動いています。これも気が向いたらメモを残しておこうかと・・・・

WEBアプリテンプレート(django+javascript)

code
code
nginx
nginx
Dockerfile
Dockerfile
docker-compose.yml
docker-compose.yml
conf.d
conf.d
uwsgi_params
uwsgi_params
project.conf
project.conf
project.conf
project.conf
/
/
[PROJECT]
[PROJECT]
docker
マウントポイント
docker…
サイト構築
ポイント
サイト構築 ポイント
プロジェクト構成
プロジェクト構成
Text is not SVG – cannot display
# 通常起動
$docker-compose up -d
# 再構築
CLEARSITE=1 docker-compose up -d
テンプレート表示状態

WEBアプリの勉強を始めた所、djangoを用いて同じ構成のサイトを何度も構築するのが面倒でテンプレートを作ることにした。しかしコレばっかりに夢中で肝心の勉強の方がおろそかになってきているのは、いつものお約束。

テンプレート構成

  • docker image 仕様(Dockerfile)
    • debianベースのpythonスリムイメージをベースとする
    • pythonライブラリインストールステージ:dbアクセス用ライブラリの為に一時的に開発ツール一式をインストール(後にスリム化処理)
    • nodejsは一旦aptパッケージインストールした後、npmで最新のバージョンをインストール
    • npmでtypescript、sass、bootstrapをインストール
    • その他チョットしたツール:gitやwget、アプリ実行用ユーザー(user)の作成とsudo権限設定
    • 実行コマンドは/code/startupシェルスクリプト:イメージにはダミーのシェルスクリプトを格納しているが、実際はサイト構築テンプレートとwusgi起動を含むシェルスクリプトを/code/startupとして作成しマウントして実行(後述)。
  • 起動:docker-compose構成
    • djangoは/codeをマウントしてサービスを実行
    • 環境変数CLEARSITE=1とすることでサイトを削除してテンプレートで再構築する(危険コマンド)
    • 構築用のstartupスクリプトが長いため、djangoサービスが始める前にnginxからのサービス要求がありロックすることがあったので、ヘルスチェックを行うようにしている(uwsgiのプロセスチェック)。
  • startupスクリプト(テンプレート構築&サービス起動)
    • 環境変数でサイト名アプリ名を定義し、サイト名のディレクトリが存在しない場合、またはCLEARSITEでアプリを削除している場合、テンプレートを用いてアプリケーション雛形を構築する。
    • Django-admin startupprojectでサイトを構築、settings.pyの必要な部分を順次編集。
    • python manage.py startappで最初のアプリケーションを作成(これを用いてシングルページアプリケーションの雛形とする)
    • アプリケーションの設定部分をsettings.pyに追記編集。bootstrapなどのミドルウェアの設定、LOGGING設定、環境変数読み込み設定。
    • ルート用urls.py、フロント側にデフォルト変数を送る機構context_processors.pyの雛形作成
    • アプリケーション定義用urls.py、views.pyの雛形作成、indexページとajax待ち受けの最低限構成、およびテンプレートHTMLのindex.html、base.html雛形作成。
    • python manage.py migrate :sqliteデータベースの初期化、管理者ユーザーの作成
    • スタティック領域の構成
      • bootstrapはnpmのグローバルエリアからローカルコピー(これは最新版に対応するため常に行う)
      • サイト用カスタムscssのテンプレート作成(再構築時)
      • sass常駐コンパイラの起動(上記scssファイルの更新と同時に自動コンパイル)
      • bootstrap関連その他js、およびtinymce設定
      • DOM構築用javascriptモジュールテンプレート(TSソース)設置、およびTypeScript初期化と設定ファイル初期編集(再構築時)
      • tsc常駐コンパイラ起動

テンプレートに凝りすぎてほぼほぼ自由度が下がってしまっている件。前回のDOM構文もTypescriptに移植して使える状態にして用意してます。細かい解説は折りを見て追記します。

室内灯自動点灯システム

システムなんて言うほど大した物ではないが、先日室内灯の蛍光灯が切れたのを機会に、LEDシーリングライトに交換した(日立LEC-AH08U)。お安い割に明るさ調整や色も白から電球色まで変えられるです。

付属のリモコンでON/OFF出来るのだが、リモコンが小さく、さらにボタンも小さくて押しにくい感じ、暗い部屋に来ていざ点けようとするとマゴマゴ感が否めない。そこでせっかくのリモコン受信付きの室内灯ということで人感センサーで自動で点灯する仕組みを作ることにした。

計画内容

  • 基本的にリモコンは使用せず、光量や色合い設定の為だけとする。
  • Arduinoなどのワンボードマイコンを使用して学習リモコンの要領でIR LEDを発光させる
  • 人感センサーでONさせるがOFFはしない(トイレの人感センサーライトで散々経験しているが、人がじっとしていると消灯してしまうので)。OFFの仕組みは別に用意する(フォトレジスタに手をかざしてOFFにする仕様)。
  • センサとLEDを仕込んだ適当なケースを3Dプリンタで製作

学習リモコン仕様

  • Arduino Nano(安い互換品)を使用
  • 人感センサー:‎ARCELI00595
  • IR LED:uxcell IRLED
  • IR受光素子:HX1838(リモコンコード取得用)
  • フォトレジスタ uxcell GL5528

右が配線図。受光素子はブレッドボード環境で実験的にリモコンコードを読み込むために使用することとし、実際の装置には組み込まない(装置としての学習機能は無し)

学習リモコン配線図

リモコン信号の学習

Arduinoのライブラリとして、ライブラリマネージャから赤外線リモコンのためのIRremoteをインストールすると、サンプルで付いてくる「ReceiveDump」を使用すると、使用したいリモコン信号を取得することが出来る。Arduino開発環境からシリアルモニタを起動しておくと下記のようにリモコン受信データの詳細がレポートされる。下記の例は今回ターゲットとしているシーリングライトのリモコン信号(ON)の詳細。

Protocol=PULSE_DISTANCE Address=0x0 Command=0x0 Raw-Data=0x3DC2FB 88 bits LSB first

Raw result in internal ticks (50 us) - with leading gap
rawData[180]: 
・・・・・
Raw result in microseconds - with leading gap
rawData[180]: 
・・・・・
Result as internal ticks (50 us) array - compensated with MARK_EXCESS_MICROS=20
uint8_t rawTicks[179] = {65,33, 10,24, 9,7, 9,8, 8,9, 8,9, 8,8, 8,9, 8,9, 8,9, 8,8, 8,9, 8,9, 8,25, 8,9, 8,8, 8,9, 8,9, 8,9, 8,8, 9,8, 9,24, 9,24, 9,8, 9,8, 9,8, 8,8, 9,8, 9,8, 8,25, 9,8, 8,25, 8,9, 8,25, 9,24, 9,24, 9,25, 8,8, 9,24, 9,8, 9,25, 8,8, 9,8, 9,8, 8,9, 8,8, 9,8, 8,9, 8,9, 8,25, 8,25, 8,25, 9,24, 9,24, 9,25, 8,25, 8,25, 9,8, 9,8, 8,25, 8,9, 8,8, 9,8, 9,8, 8,9, 8,25, 8,25, 9,8, 8,25, 9,25, 8,25, 8,25, 8,25, 9,8, 8,25, 9,8, 8,9, 8,9, 8,8, 8,25, 9,25, 8,25, 8,9, 8,25, 8,25, 8,25, 8,25, 9,8, 8,9, 8};  // Protocol=PULSE_DISTANCE Address=0x0 Command=0x0 Raw-Data=0x3DC2FB 88 bits LSB first

Result as microseconds array - compensated with MARK_EXCESS_MICROS=20
uint16_t rawData[179] = {3230,1670, 480,1220, 430,370, 430,420, 380,470, 380,470, 380,420, 380,470, 380,470, 380,470, 380,420, 380,470, 380,470, 380,1270, 380,470, 380,420, 380,470, 380,470, 380,470, 380,420, 430,420, 430,1220, 430,1220, 430,420, 430,420, 430,420, 380,420, 430,420, 430,420, 380,1270, 430,420, 380,1270, 380,470, 380,1270, 430,1220, 430,1220, 430,1270, 380,420, 430,1220, 430,420, 430,1270, 380,420, 430,420, 430,420, 380,470, 380,420, 430,420, 380,470, 380,470, 380,1270, 380,1270, 380,1270, 430,1220, 430,1220, 430,1270, 380,1270, 380,1270, 430,420, 430,420, 380,1270, 380,470, 380,420, 430,420, 430,420, 380,470, 380,1270, 380,1270, 430,420, 380,1270, 430,1270, 380,1270, 380,1270, 380,1270, 430,420, 380,1270, 430,420, 380,470, 380,470, 380,420, 380,1270, 430,1270, 380,1270, 380,470, 380,1270, 380,1270, 380,1270, 380,1270, 430,420, 380,470, 380};  // Protocol=PULSE_DISTANCE Address=0x0 Command=0x0 Raw-Data=0x3DC2FB 88 bits LSB first

uint16_t address = 0x0;
uint16_t command = 0x0;
uint32_t data = 0x3DC2FB;
・・・・・

この例ではプロトコルがPULSE_DISTANCEとなっており、サポートしているメーカーフォーマットでは無いっぽいため、rawDataとして直接送信する必要がある。ターミナルに出力された詳細データのrawDataはそのままCのソースに貼り付けられる形式となっている。

// 初期化
IrSender.begin(IR_SEND_PIN);
// 送信部
uint16_t rawData[179] = {
  3230,1720, 380,1270, 380,470, 380,420, 480,370, 430,420, 430,420,
  430,420, 380,420, 430,420, 430,420, 430,420, 380,470, 380,1270,
  380,470, 380,420, 430,420, 430,420, 430,420, 380,470, 380,420,
  430,1270, 380,1270, 430,420, 380,470, 380,420, 430,420, 430,420,
  430,420, 380,1270, 430,420, 380,1270, 430,420, 430,1220, 430,1270,
  380,1270, 380,1270, 430,420, 380,1270, 430,420, 430,1220, 430,420,
  430,420, 430,420, 380,470, 380,420, 430,420, 430,420, 380,470, 380,1270,
  430,1220, 430,1270, 380,1270, 380,1320, 380,1270, 380,1270, 380,1270,
  430,420, 430,420, 380,1320, 380,420, 380,470, 380,420, 430,420, 430,420じ,
  430,1270, 380,1270, 380,470, 380,1270, 380,1270, 430,1270, 380,1270,
  380,1320, 380,420, 430,1270, 380,420, 430,420, 380,470, 380,470, 380,1270,
  380,1270, 430,1270, 380,470, 380,1270, 380,1270, 380,1270, 430,1270,
  380,420, 430,420, 380
};
IrSender.sendRaw(rawData, sizeof(rawData) / sizeof(rawData[0]), 38);

Arduinoのプログラムでは人感センサーの出力に連動してON信号を送信、フォトレジスタのアナログ入力レベルが一定以下になったとき(手をかざすなど)にOFF信号を送信する仕組みとした。

フローチャート(draw.io)
START
START
初期化
初期化
Yes
Yes
No
No
readmode?
readmode?
受信モード初期化
受信モード初期化
送信モード初期化
送信モード初期化
Loop
Loop
Yes
Yes
No
No
readmode?
readmode?
Yes
Yes
受信データあり
受信データあり
受信データ
ターミナル出力
受信データ ターミナル出力
光センサ
光センサ
On
On
Off
Off
LIGHT Mode
LIGHT Mode
LIGHT OFF CMD
LIGHT Mode Off
LIGHT OFF CMD…
Off
Off
Off
Off
LIGHT Mode
LIGHT Mode
5000ms
5000ms
Yes
Yes
No
No
人感センサ
人感センサ
LED インジケータON
LED インジケータON
1500ms
1500ms
Yes
Yes
人感センサ
人感センサ
LIGHT ON CMD
LIGHT ON CMD
1000ms
1000ms
500ms
500ms
LED インジケータOFF
LED インジケータOFF
100ms
100ms
LIGHT Mode On
LIGHT Mode On
Text is not SVG – cannot display

←ソースコードと簡単な資料はこちら

完成した装置全貌

3Dプリンタでザックリ作ったボックスと内部配線。

ボックスの上部にリモコン発信用赤外線LEDとOFF感知用フォトレジストを埋め込んでます。

リモコンが小さすぎて操作しにくいなら、従来通り壁スイッチで良いじゃん!というのはごもっともなお話ですが、それに気づいたのが一通り完成して運用し始めてからでしたw
あと、最近の半導体不足でArduinoが純正品しか手に入らず比較的高価なため、安価なRaspberryPi Picoに移植するべく開発環境など準備中・・・てか次の投稿ネタに保存中。。

FFmpeg on Docker

久々の投稿

dockerイメージでFFmpegをフルコンパイルしてみました。第1の理由は興味本位ですが、あとSVTAV1のライブラリを使用したいのと、tarボールからコンパイル&インストールして通常使用の環境が汚れるのをある程度防ぎたかったというのもあり、実験環境としてのDockerで行うこととした。副次的な効果として、Windows環境やMac環境、ひいてはRaspberryPi環境のどれか空いてるマシンで動画変換作業をさせることが出来る。

Dockerfileの解説

ベースとするイメージはpython:3.10.3-slim-bullseyeでDebianスリムイメージを利用しました。

pythonのビルドと同様に

  1. 現在のaptパッケージを保存(python環境など)
  2. aptで開発環境をドヤ−−−−っとインストール
  3. debianのライブラリにはfdk-aacがパッケージとして存在しないので、sourceforgeからソースコードをダウンロードしてビルド&インストール
  4. 開発環境が一通り完成したところで、SVT-AV1をgithubからcloneしてビルド&インストール
  5. ライブラリが揃ったので、ひとまずldconfigしてから、目的のffmpegをこれもgithubからcloneしてビルド&インストール
  6. このままではDockerイメージサイズが大変なことになっているので、不必要なパッケージを削除します。
  7. apt-markコマンドを使用して、2番以降でインストールしたパッケージをauto-remove対象にします(1番以前のパッケージは対象としない)。
  8. /usr/local 以下にある実行ファイルに関してlddコマンドを使用して関連するダイナミックライブラリをリストアップ、さらにそれらを含むaptパッケージを検索してリストアップしてauto-remove対象外にする。
  9. aptコマンドを使用しauto-removeで不必要なパッケージをゴッソリ削除
  10. イメージのスリム化を行った所でステージを分けて、Python用の必要パッケージをインストールしてffmpeg-pythonなどが利用できるようにする

イメージのスリム化のための仕組みはベースとしたpython:3.10.3-slim-bullseyeを参考に・・・というか殆どパクりで使用しました。1〜9までを一つのRUN文で行うのがミソ。RUN文で分けた方が見やすいけど、ビルドのステージが分かれてしまうのでステージが変わってからパッケージを削除してもイメージサイズが変わらないというの何とも。。

ビルドファイルの開発中などは複数のステージに分かれていた方がデバッグしやすくて良いが、一通り完成したら一つのRUN文にまとめてしまう感じですかね。

HEIF→JPEG変換

ブログへの写真アップロードの件で少し考えたのが、以前のスマートフォンをiPhoneに変えたときに写真の画像ファイルフォーマットがデフォルトではjpegではなくheifになっていたので、互換性を重視するためにということでjpegに変更していた事。コマンドラインで一括変換する仕組みを導入するにあたって、改めてheifフォーマットの利点を調べてみた。ウィキペディアなどで調べてみると、画質はそのままで圧縮率が2倍以上と、保存用途としてもかなり良いため再びheifでの保存に切り替えることとする。

で、必要になってくるものがpythonでheifを扱うためのライブラリ’pyheif’

とりあえずbrewでインストール出来るということで、まずは必要なライブラリから

$ brew install libffi libheif (libde265) # 括弧はWSL2の場合

その後にpipでpythonライブラリのインストール

$ pip install pyheif

と、WSLではうまく行ったのだが、自分の環境のMacOSではpipでインストールエラーとなった。どうやらclangのインクルードパスが/opt/local/includeを要求しているのに対し、それが無いためらしい。内容を調べてみると、/opt/homebrewというディレクトリが/opt/localであれば色々解決しそうなので、試しにシンボリックリンクを張ることとした。

$ sudo ln -s /opt/homebrew /opt/local

これで旨く/opt/local/includeを見つけることが出来、pyheifライブラリがインストール出来た。

次にサイトを参考に先日のアップロード準備用リサイズプログラムwpresizeにheifデコード機能を組み込んでみた。

from PIL import Image
import pyheif

heif_file = pyheif.read("sample.heic")
image = Image.frombytes(
    heif_file.mode, 
    heif_file.size, 
    heif_file.data,
    "raw",
    heif_file.mode,
    heif_file.stride,
    )
image.save("output.jpeg", "JPEG")

ただし上記はあくまでも画像を変換するだけで、Exif情報はコピーされない。そこでpyheifからExif情報を取り出して出力JPEGファイルに付加する方法を調べてみた。

heif_file(pyheifオブジェクト)にはその他にmetadataというプロパティがあり、メタデータを格納する辞書形式(のリスト)であると説明される。リストと言っても内容は一つしかなく、そのゼロ番目の内容を調べると

type: 'Exif'
data: xxxxxx(バイナリ情報)

であったため、このdataの中身が撮影画像のExif情報だということが分かる。これを保存時に付加することでExif付きで保存することが出来る。

.....
exif = heif_file.metadata[0]['data']
.....
image.save("output.jpeg", "JPEG", exif=exif)

GPS情報だけ消したり、Exif情報を編集したいのであれば、前回同様に一度辞書形式に変換してやれば編集可能となる。

exif = heif_file.metadata[0]['data']
exif_dict = piexif.load(exif)
del exif_dict['GPS']
exif = piexif.dump(exif_dict)
image.save("output.jpeg", "JPEG", exif=exif)

結局メタデータ自体が2重に辞書保管されるような形になっていたのが分かりにくかったです。

あともう一点注意すべき点は、Exifの記載されている画像の回転情報。カメラ構え方で画像の縦横を変えて表示したいが、オリジナル画像から縦横変換をしようとすると画素の並び順から変えなければならず、再圧縮からの点からも非効率的(勝手な想像)だと思うので、Exifの情報として画像をどの様に表示するのかの向き(回転)情報を保管している。サムネイルやビューワはこの情報をもとに画像を表示している(GIMPなどの編集ソフトはこの情報をもとにするが、向きに合わせてオリジナルのピクセル情報を並び替えるのかを聞いてくる)。

そういった意味かと思うが、heif_fileで変換されたイメージはExif情報は保存されないが、Exifの回転情報をもとにピクセルを並び替えているようで、オリジナルを右90度回転した縦長画像を変換しても、縦長画像として出力される。これに対して右90度回転したExif情報を付加すると、さらに画像回転情報が加わり横倒しになった画像になる。

この辺りの仕組みを変換時に制御出来るかどうか調べられなかったので、とりあえずは変換時にオリジナルの回転処理がされるものとして、保存するExif情報のうち回転情報のみ1(正転)に戻しておくこととした。

exif = heif_file.metadata[0]['data']
exif_dict = piexif.load(exif)
del exif_dict['GPS']
exif_dict['0th'][274] = 1  # 回転情報を保持するExifアドレス(右回転の場合6になってるので正転1とする)
exif = piexif.dump(exif_dict)
image.save("output.jpeg", "JPEG", exif=exif)

対処療法的ではあるが、とりあえずこれでExifを保管し画像の向きを保持して変換出来るようになった。

前回のwpresizeに組み込んだところがこちら(masterにコミットしてしまったので前回のリンクもheif対応の物になってますw)。

pyheifオブジェクトに関しては詳しい資料が見つからなかったので、自分で調べる形になってしまった。とりあえず結果オーライで暫く運用ということで

2022年明けましての投稿

新年のネタは、3Dプリンタによる鏡餅。丁度白シルクのフィラメントがあったので8cmサイズで製作しました。ミカンは本物です。A4ペーパーを二つ折りにして、裏白はインクジェット用OHPシートに印刷してそのまま乗せてますw

昨日の投稿では旅行物だけあった写真が多く、貼り付ける写真について一枚一枚、GIMPで開いてサイズ変更して上書きセーブしてアップロードという手順を取っていたため非常に面倒で時間のかかる作業だった。前々から気になっていたが何となく惰性で過ごしていた感がある。

それに加えて、こちらの環境ではSafariとWorpressのライブラリの相性が悪く、非常に反応が遅く、下手するとハングアップしてしまうので、このブログ書きの時だけMicrosoftのEdgeを起動していたりします。

その部分は差し当たっては仕方の無いことで、追々と問題を解決しなければと思うところですが、とりあえず画像のサイズ変換アップロード環境だけは早急に何とかしないと、ということで、pythonスクリプトで画像サイズ変換プログラムを自作しました(リサイズだけでたいしたことはしていない為)。

Pillowという画像ライブラリとpiexifというExif編集ライブラリを使用しています。長手方向の幅を2000以下としてファイル名を変えて(-fオプションを付けるとオーバーライト)保存する単純なプログラム。

ついでにExif情報のうちGPS情報を削除します。

まぁ自分の一眼レフカメラはGPS持ってないし、iPhoneの写真をMacからアップするときは写真をフォルダコピーした段階でGPS情報は消されているっぽいので、普通には問題無いのだが、OneDriveのカメラロールに保存された写真とかたまにGPS情報がベッタリ貼られたまま流通しがちなので注意が必要です。

Pillowとpiexifを使用したExif編集の仕組みは以下の通り。

import piexif
from PIL import Image

img = Image.open('sample.jpeg')
exif = img.info['exif']
exif_dict = piexif.load(exif).  # Exif辞書情報の取り出し

# 取り出した辞書情報を編集します
del exif_dict['GPS']     # GPS情報を削除

# 編集後保存可能なバイナリデータに戻します
exif = piexif.dump(exif_dict)

# リサイズなどした新しい画像データの作成
img_new = img.resize([with, height], Image.BICUBIC)

# 編集したExifを適用して保存します
img_new.save('output.jpeg', quality=95, exif=exif)

久々のプログラミング。そういえばMacでの開発環境は作っただけで実践は初めてだった。。。いろいろライブラリとか入れ忘れてたりして意外とドタバタしました。

EC6とDOMの件

EcmaScript6(javascript)の勉強の件で、DOMの記載に関して考えてみた。以前のjavascriptではjQueryなどのライブラリを使ってDOMを簡潔に記載していたが、EC6以降も豊富なライブラリを使う機会が増えているような感じ。

しかし、折角なので、ここはライブラリを使用せずに素のEC6でDOMを分かりやすく記述出来ないものかと考えてみた。通常、素のjavascriptでDOMを記載するときは、例えばBODYタグから下を全てDOMで記述する場合

const body = document.querySelector("body"); // まずBODYタグを見つける
var title = document.createElement("h1");    // タイトル用のH1タグを生成
title.innerHtml = "page title";              // タイトル文字を設定
body.appendChild(title);                     // タイトルをBODYの子要素とする
var content = document.createElement("p");   // 本文段落用タグを生成
content.innerHTML = "page content";          // 本文を設定
body.appendChild(content);                   // 本文をBODYの子要素とする

の様に、既存のタグを見つけるか、新規タグを生成して内容を設定し、親要素に接続する作業を繰り返して文書構造を組み立てていく形になるが、階層が深くなると何が何だか分からなくなってしまう。そのあたりをjQueryでは文書構造に沿った形で記述出来て便利なのだが、”$”関数の数珠つなぎとなりなんだか違う言語のような気がして、個人的には好きにはなれなかった。そこでEC6ではライブラリを一切用いずに次のように記載してみた。

((body) => {                            // bodyを引数とした矢印関数①
  body.className='hogehoge';
  body.appendChild(((title) => {        // bodyタグへのappendChildを同様に矢印関数で記述②
    title.innerHtml = "page title";
    return title;
  })(document.createElement('h1')));
  body.appendChild(((content) => {      // 同様に本文ダグも矢印関数で生成と同時にappendChild
    content.innerHtml = "page content";
    return content;
  })(document.createElement('p')));
  return body;                          // ①関数の戻り値(これが無いと結果が空になる)
})(document.querySelector('body'))      // ①関数の引数として body = document.querySelector('body')

同様の内容を文書構造を意識した記載方法にしてみた。矢印関数を用いてタグを生成(またはquery)し、タグの内容を関数内で記載。子要素も矢印関数内で新たに矢印関数で生成したタグをその場でappendChildする。

かなり冗長な記述になるが、文書構造がHTMLに近い形で多少は見やすくなるのではと考える。

というか、そもそも文書全体をDOM化しようとするからイケないんですけどねw
あと、Lispかっ?!てくらい括弧がメチャメチャ多くなるので、エディタの力を借りないと括弧が一個違うとかでバグ探しも大変なことになりそう。。。vscodeさんありがとう!

不正アクセスログ集計ツールその後

以前の記事で不正アクセスの疑いのあるIPアドレスを集計してアクセスブロックするツールを作成し運用していることと、docker導入に当たってブロックの仕組みを再検討して運用している記事を上げていましたが、もう一つポータルサイトのWEBアプリへの不正アクセス(django管理画面やwordpressへの不正ログイン)を集計する仕組みも紹介しました。実はこの仕組みに関してもdocker化で動作しなくなっていました。

データベースに保管された不正ログイン情報(Login_History)は別コンテナのデータベースサーバーにあるので、localhostからコンテナ名に変更するだけで良いのだが、ブロック済みのIPアドレスリストを取得して新規IPアドレスのみを表示する仕組みについては、ブロックリストがホストで管理されているためコンテナ内からはアクセス出来ない。ブロックリストを保管している/etc/ufw/before.rulesをコンテナに強引にマウントしてしまう方法もあるが、あまりスマートではない。

ということでこれまた興味本位の勉強を兼ねてホスト側からdockerブリッジネットワークへのサービスとしてソケット通信でブロックリストを公開する仕組みを作成した。

  • blocklist-server
#!/usr/bin/env python3
#--*-- coding: utf-8 --*--
# 対象ネットワーク: docker bridge network([shared] 192.168.29.0/24)
# ブロックリストホスト: 192.168.29.1
# 起動(su): /usr/local/sbin/blocklistsrv.py | /usr/bin/logger -i -t blocklistsrv 2>&1 &
# 停止: ps aux | grep blocklistsrv  -> kill -9
# ポート確認: lsof -i :8765  -> kill -9

import os, sys, socket
import psutil
import argparse
import re
import numpy as np

import daemon
from daemon import pidfile

pfile = '/var/run/blocklist-server.pid'

ipset_ufw = '/etc/ufw/before.rules'
ipset_start_line = 'blacklist-ipset-S'
ipset_end_line = 'blacklist-ipset-E'

svport = 8765
svhost = '192.168.29.1'

def getArgs():
    parser = argparse.ArgumentParser('blocklist service cmd')
    parser.add_argument('cmd', nargs='?', help='(start|stop|restart)')
    parser.add_argument('-l', '--syslog', action='store_true', help='enable syslog output')
    return parser.parse_args()

args = getArgs()

ーーー省略ーーー
def blserver():
    socket1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    socket1.bind((svhost, svport))
    socket1.listen(5)

    logger.info('Start Blocklist server. port : %d', (svport))
    while True:
        try:
            clientsocket, address = socket1.accept()
            blist = []
            logger.info(f"Connection from {address} has been established!")
            with open(ipset_ufw, 'r', encoding='utf-8') as fi:
                for line in fi:
                    if re.search(ipset_start_line, line):
                        break
                for line in fi:
                    if res:= re.match('^-A ufw-before-input -s ([0-9./]+) .*', line):
                        #logger.debug(res[1])
                        blist.append(res[1])
                    if re.search(ipset_end_line, line):
                        break
        #except KeyboardInterrupt:
        #    #clientsocket.close()
        #    exit('exit with keyboard interrupt!')
        except FileNotFoundError:
            logger.error('file not found! : %s' % (ipset_ufw))
        except PermissionError:
            logger.error('can\'t read %s, please run as superuser!' % (ipset_ufw))
        finally:
            clientsocket.send('\n'.join(blist).encode('utf-8'))
            clientsocket.close()

        ## blist 送信 

def main():
    #args = getArgs()
    #logger = getLoggerConf(args.syslog)
    if args.cmd == 'start':
        pass
    elif args.cmd == 'stop' or args.cmd == 'restart':
        # デーモンストップ
        if os.path.exists(pfile):
            try:
                logger.info(pfile + " is exits")
                pid =  open(pfile, 'r').read().rstrip()
                proc = psutil.Process(int(pid))
                proc.terminate()
                os.remove(pfile)
                logger.info('pid : %s removed.' % pid)
            except PermissionError:
                logger.error('PID File, permission error, please run as super user.')
                exit(0)
            #except:
            #    logger.error('some error occured.')
        if args.cmd == 'stop':
            exit(0)
    else:
        logger.error('operation command is one of (start|stop|restart)')
        exit(1)

    # デーモン起動
    with daemon.DaemonContext(pidfile=pidfile.TimeoutPIDLockFile(pfile)) as context:
        logger.info(context.is_open)
        blserver()


    #exit(0)



if __name__ == '__main__':
    main()


start, stop, restart機能を持つデーモンスクリプトを作成し、ubuntuホストへサービス登録

  • /usr/lib/systemd/system/blocklist.service
[Unit]
Description=blocklistsrv: this is service of ufw blocking IP address list.
After=docker.service

[Service]
ExecStart=/usr/local/sbin/blocklist-service -l start
ExecStop=/usr/local/sbin/blocklist-service -l stop
ExecReload=/usr/local/sbin/blocklist-service -l restart
Restart=no
Type=simple

[Install]
WantedBy=multi-user.target

ここでsudo systemctl start blocklistでサービスが起動される。ちなみに恒久的に起動するようにするにはsudo systemctl emable blocklistとする。

クライアントであるdocker内のdjangoのコードも下記のようにソケット対応に変更。

blocklist_socket_server = ('192.168.29.1', 8765) 

def get_blocklist():                                                                                                    
    """                                                                                                                 
    root権限でブロックリスト取得コマンドを走らせて標準入力からブロックアドレスのリストを返す                            
    """                                                                                                                 
    socket1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)                                                         
    socket1.connect(blocklist_socket_server)                                                                            
    bipset = [ ipv4adrset(ips) for ips in socket1.recv(4096).decode('utf-8').split('\n') ]                              
    return bipset                                                                                                       

とりあえず動作するようになったので一安心。

名前付きなUNIXソケットでも良かったけど、またソケットファイルをコンテナにマウント〜となってしまうので、dockerネットワークのゲートウェイから見せるソケットサーバーとしました。こんな感じで良いのだろうか。まぁ特に重要なサービスでもないので結果オーライで運用中です。

IEさようなら

こちらのブログはwordpressを使っているので、まだ大丈夫っぽいですが、ポータルサイトは先月辺りにBootstrap5にアップグレードしたのでいよいよIEでは見られないサイトとなってきました。具体的にBotostrap5のどこがIEでダメなのかはよく調べてないですが、とりあえずjavascriptの記述はmodule式にしたのでIEでは動作はしません。

bootstrap5からjavascriptの形式としてesm(EcmaScriptModule?)が追加されているので、これはモジュールとして使用する必要があります。この場合従来では

<head>
    ・・・・・
    <meta charset="UTF-8">
    <link rel="stylesheet" href="css/bootstrap.css">
    <script type="text/javascript" src="js/bootstrap.bundle.js"></script>
    ・・・・・
</head>

上記のようにヘッダで設定していたものを

<head>
  ・・・・
  <style type="text/css">
    @import '/css/bootstrap.css';
  </style>
  <script type="module">
      import { Alert, Button, Carousel, Collapse, Dropdown, Modal, Offcanvas, Popover, ScrollSpy, Tab, Toast, Tooltip } from "./js/bootstrap.esm.js";
  </script>
  ・・・・
</head>

と、新しい形で書いてあげる必要があります。

まぁ、esmじゃ無い方を使えば従来式で書けるんだけど、まぁ新しいモノ好きということで。。。

Three.jsによる3Dオブジェクト表示

Three.js +Orbitcontrol.js + GLTFLoader.js を使用。モデルはFFXImodelviewer2を使用してmqo形式で取り出してBlenderにて明るさ調整をしつつglbで出力、WEBアプリにて表示している

FF11をやっていた頃の自キャラっぽいモデル。サンプル用だけど一応コピーライト表示しておきますが、不味かったら消します。

少し時間が空いてしまったが、自サイト構築としてdjangoによるポータルの仕組みがある程度出来てきたところで、次はフロント側の勉強をしておりました。

フロント側はjavascriptになるため、ローカル環境で試しながら少しずつ勉強をしてきた。

今回はポータル側のdjangoアプリから埋込みサービスを作成して、こちらのwordpressで表示するテストを行った。

iframeタグにてポータル側のURLを指定してjavascriptのThress.jsライブラリを用いてWEBGL用フォーマットglTFの3Dオブジェクトを表示させている。

wordpressの方をもう少し勉強すれば埋め込みを使用せず、直接javascriptのプラグインを作ってしまえるのだが、それはまたいずれということで、まずはここまで勉強した成果を残すことを優先しました。

いずれ3Dオブジェクトをリアルタイムに操作してサーバー側とのやりとりをしながらのアプリ作成を目論んでこういう仕組みとしてみたということで。。。

このキャラです。同じ帽子を被せたかったのですが、モデルに帽子を被せるとポニーテールが飛び出してしまったので今回は無しで。。。