プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

SpatialChat 使ってみました

少し前になりますが、2021年1月9日(土)に、第22回伊勢IT交流会をオンラインで開催しました。

第22回伊勢IT交流会(オンライン) - connpass

昨年はコロナの影響もあり一度も開催できませんでしたので、今回はオフラインでの開催をあきらめて、初めての試みとしてオンラインでの開催としました。

今回のオフラインイベントでは、 SpatialChat というものを使ったので、それについて思ったことを徒然なるままに書き留めておこうと思います。

SptialChat について

ご存じの方も多いと思いますが、伊勢IT交流会の少し前に知り合いから SpatialChat の存在を教えていただきました。

zoom とかの Web 会議システムと違うのは、ミーティングに参加すると、ある広さの仮想空間に自分が丸で囲われたアイコンで表示されて、その空間内を自由に動けるというスタイルである点です。で、その際に相手との距離に応じて、聞こえる声の大きさが変わるというものです。

離れすぎると声が聞こえなくなります!

まあ、文章で書いてもわかりにくいのですが、実際の画面を見ると一目瞭然と思います。こんな感じです。

f:id:junichim:20210109222514p:plain

こういう形なら、現実の交流会と同じようにグループごとに集まって雑談とかもできるのではないかと思い、今回使ってみました。

使い方

参加者がミーティング用のスペース(ルームとも呼ばれます)に参加するには、参加用のURLをクリックして、表示名を設定するだけなので、非常に簡単です。

一度、SpatialChat を試したいというのであれば、こちらの URL から参加することができます。この URL から入るルームは誰でも入れる公開ルームになります。

ルームを自分で開けば、参加者を限定して会議を行うことができます。ルームを開くにはメールアドレスを入力してルーム名を決定する必要があります。

ルームへ参加した参加後は、画像、動画、デスクトップの共有もできます(また、ルームの主催者はこれらの許可をコントロールできます)。

LTで使う資料を共有するような場合は、自分のPCのデスクトップに全画面表示して、それを共有する形になります。zoom とかと同じですね。

面白いのは、デスクトップの共有を複数人が同時に行うことができるので、ルームの両端で同時に2つの LT をするような使い方も不可能ではありません。

トラブル

新しいサービスなので、トラブルもありました。

LT をしている最中に、デスクトップ共有が半透明の黒い画面になってしまい、表示をみることができませんでした。参加した何名かの方で起きていたようです。

サービスが推奨しているブラウザ としては、 Chrome ですが Chrome でも問題が起きていました。自分の手元での環境だと Firefox で表示したほうがこの現象が軽減されている印象でした(Firefox でも起きてましたけどね)。

このあたりは今後の改善を期待したいところです。

一方、音声の品質は全然気になりませんでした。

まったくの自分の主観ですが、ある程度の人数が参加している場合、 zoom や Skype での音声のほうが、品質があまりよくない方がいたように見受けられるような気がします。まあ、たまたまかもしれませんけどね。

他のツールと比べて音質がどうなのかを客観的に比較できるものがあるといいんですが、どうなんでしょうかね?

所感

さて、上記で少し触れたように、今回 SpatialChat を選んだ理由である、オフラインでの交流会のようにグループごとに雑談をしてもらいたいというのは、実現できていたでしょうか?

残念ながら今回はあまりできていなかったかな?という印象です。

いくつか、気になった点を挙げると、

  • 人数が多くなってきても、グループごとに分かれていかず、全体で一つのグループになってしまっているようだった
  • ひとりぽつんといる方がちらほらと見受けられた
  • もともと LT の件数が、それほど多くないことを想定していたのですが、予想外に LT 件数が増えて、そのため雑談の時間があまりとれなかった
  • LT ベースだと、話すほうも、進行も、質問する人も、全体に向けて話す形になる

というあたりです。

2つめの点に関して、初めての人が孤立しがちになるのはオフラインでも同じで、本来であれば主催者である私がフォローするべき点です。 ですが、参加者が問題なく参加できているかなどが気になってしまい、そこまで手が回っておりませんでした。反省点です。

また、後者の2点のような傾向があるなら、 zoom のような1対多で使うことが想定されるサービスのほうが、何かとスムーズに進行できるのかもしれません。

そういう意味では、このサービスは、純粋な交流会(雑談?)とか、グループディスカッション(グループワーク)のようなで使うのが向いているのかもしれませんね。

このあたりはなかなか使い分けが難しい印象です。

ご参考

なお、後日、 SpatialChat のヘルプを検索したら、共有画面が黒くなる事例についてのヘルプが載ってました。

What if my screensharing is flicking or black? – SpatialChat Help Center

ブラウザのハードウェアアクセラレーション設定を変更するというもののようです。私のほうでは試せてないのでなんとも言えませんが、もし使う方がいらっしゃれば、ご参考にしてください。解決するといいのですが。

まとめ

いろいろ書きましたが、個人的には非常に面白いサービスだと思っています。 問題点は改善して、ぜひいいサービスになるといいなと思います。

また、伊勢IT交流会もオフラインで開催する機会がまだまだあると思いますので、今後もいろいろと試したいと思います。

はてなブログのコードハイライトを SyntaxHighlighter から はてなの markdown に変更

はてなブログに移行してくる前から、Syntaxhiglighter を使っていたんですが、どうもここしばらく開発が止まっているようです。

github.com

一応、こちらの記事に書いたように v4 に対応させて使っていたんですが、最近手を出している新しめの言語、 dart (flutter で利用) とかに対応していないのが、気になってきました。 それと、スマホには対応していないんですよねー。

そんなことが重なったので、そろそろ替え時かな?と思うようになりました。 というわけで、 SyntaxHighlighter から はてなブログが提供している markdown によるコードハイライトに切り替えようと思い立ちました。

ただ、既存の記事が多数あるので手作業は嫌です。幸い、はてなブログでは、API経由で投稿・編集ができるので、それを使って、らくちんで対応したいと思います。

というわけで、移行時に行った作業のメモをまとめておきます。

API について

まず、はてなのAPIでできることを調べてみます。

はてなブログAtomPub - Hatena Developer Center

エントリー一覧の取得、個別の記事の取得、新規投稿およびアップデートと一通りのことができますね。

wget を使ってエントリー一覧をとってみます。認証は Basic 認証です。

mor@DESKTOP-H6IEJF9:~/work/hatena$ wget --user=はてなID --password=APIキー https://blog.hatena.ne.jp/はてなID/ブログID/atom/entry -O test.xml
Will not apply HSTS. The HSTS database must be a regular and non-world-writable file.
ERROR: could not open HSTS store at '/home/mor/.wget-hsts'. HSTS will be disabled.
--2020-12-23 10:15:42--  https://blog.hatena.ne.jp/はてなID/ブログID/atom/entry
blog.hatena.ne.jp (blog.hatena.ne.jp) をDNSに問いあわせています... 13.115.18.61, 13.230.115.161
blog.hatena.ne.jp (blog.hatena.ne.jp)|13.115.18.61|:443 に接続しています... 接続しました。
HTTP による接続要求を送信しました、応答を待っています... 401 Unauthorized
選択された認証形式: Basic realm="Hatena Blog"
blog.hatena.ne.jp:443 への接続を再利用します。
HTTP による接続要求を送信しました、応答を待っています... 200 OK
長さ: 特定できません [application/atom+xml]
`test.xml' に保存中

test.xml                                                    [ <=>                                                                                                                         ] 187.44K  --.-KB/s    時間 0.04s

2020-12-23 10:15:42 (4.57 MB/s) - `test.xml' へ保存終了 [191940]

mor@DESKTOP-H6IEJF9:~/work/hatena$

こんな感じで XML が返ってきました。

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:app="http://www.w3.org/2007/app">

  <link rel="first" href="https://blog.hatena.ne.jp/はてなID/ブログID/atom/entry" />


  <link rel="next" href="https://blog.hatena.ne.jp/はてなID/ブログID/atom/entry?page=xxxxxxxxxxx" />


  <title>プログラマーのメモ書き</title>

  <subtitle>伊勢在住のプログラマーが気になることを気ままにメモったブログです</subtitle>

  <link rel="alternate" href="https://blog.mori-soft.com/"/>
  <updated>2020-12-17T17:20:51+09:00</updated>
  <author>
    <name>junichim</name>
  </author>
  <generator uri="https://blog.hatena.ne.jp/" version="4d0fe3f3cd3a840afb05e29c86af788f">Hatena::Blog</generator>
  <id>hatenablog://blog/yyyyyyyyyyyyyyyyyyyyyyy</id>


  <entry>
<id>tag:blog.hatena.ne.jp,2013:blog-はてなID-xxxxxxxxxxxxxxxxxxxxxx-yyyyyyyyyyyyyy</id>
<link rel="edit" href="https://blog.hatena.ne.jp/はてなID/ブログID/atom/entry/エントリーID"/>
<link rel="alternate" type="text/html" href="https://blog.mori-soft.com/entry/2020/12/23/002700"/>
<author><name>junichim</name></author>
<title>はてなブログのコードハイライトを SyntaxHighlighter から はてなの markdown に変更</title>
<updated>2020-12-21T16:02:21+09:00</updated>
<published>2020-12-18T10:55:34+09:00</published>
<app:edited>2020-12-23T00:27:00+09:00</app:edited>
<summary type="text">はてなブログに移行してくる前から、Syntaxhiglighter を使っていたんですが、どうもここしばらく開発が止まっているようです。 github.com 一応、こちらの記事に書いたように v4 に対応させて使っていたんですが、>最近手を出している新しめの言語、 flutter…</summary>
<content type="text/x-markdown">はてなブログに移行してくる前から、Syntaxhiglighter を使っていたんですが、どうもここしばらく開発が止まっているようです。


[https://github.com/syntaxhighlighter/syntaxhighlighter:embed:cite]

一応、[こちらの記事](https://blog.mori-soft.com/entry/2018/01/31/224619)に書いたように v4 に対応させて使っていたんですが、最近手を出している新しめの言語、 flutter (dart) とかに対応していないのが、気になってきました。
それと、スマホでみると、シンタックスハイライトがうまく機能していません。

これだけではちょっとわかりにくいですが、エントリーIDの一覧が返ってくるかと思いきや、一度に複数件(これ試したときには10件、ドキュメント上は7件)の記事が完全な形で取得されています。

で、次の一覧を得るためには、取得したドキュメント内のlink要素(rel属性がnextのやつ)のURLを呼ぶという形になるようです。 pageパラメータの求め方がわからないので、順番にコレクションを取得して、次のURLを呼び、得られたコレクションにある次のURLを呼び・・・という形を用いて、すべてのエントリーを得ることができるようです。

page パラメータをダイレクトに求められれば、もうちょっと楽な気がします。

個別の記事については、entry タグに含まれている link タグから、エントリーIDを切り出せば、アップデートに使えそうです。

こういう処理は Python でやると楽そうですね。なかなか本格的に Python を使う機会もないので、手元の書籍を見ながらボチボチ組んでみます。

Python スクリプトの作成

まず、コードを書く前に、 SyntaxHighlighter での言語指定と はてなブログの markdown の言語指定の対応を調べる必要があります。

とはいっても、すべての言語の対応表を作るなんて、ばからしいので、今回はこういう風に対応することにしました。

  1. 既存の SyntaxHighlighter の言語指定を抜き出し一覧にする
  2. はてなブログの markdown で対応している言語名との対応表を作る
  3. できた対応表を用いて、 SyntaxHighlighter の記述を markdown に変換する

また、全体の処理の流れとしては、

  1. コレクションAPI にてエントリー一覧(10件分)を取得
  2. 各エントリーについて以下を実施
  3. XML をパース
  4. SyntaxHighlighter の記述があるか判定
  5. なければ、次のエントリーへ
  6. あれば、必要な処理を行う(SyntaxHighlighter で指定されている言語の取得や記述の変換)

という感じでしょうか。

API からデータを取得

Python で http 経由で何かを取得するのは urllib が楽なようです。また、はてなのAPIは認証が必要です。今回は、 Basic 認証にします。

下記記事などを参考に、 urllib から Basic 認証で API をたたいて、XML を取得します。

最初に試したとき、APIの最後にスラッシュが付いていたのに気づかず、404 が返ってきてしまい、なんでだ?となってしまいました。お気を付けください。

XML の解析

はてなのAPI からは XML が返ってきます。 Python で XML を操るには、 ElementTree を使うのが王道っぽいですね。

まずは、パースして、個別のエントリーを取得すればよさそうです。

ただ、はてなの API で返ってくる XML は名前空間が指定されているので、そこだけ、工夫してやります。

XML の名前空間はこんな感じです。

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"
      xmlns:app="http://www.w3.org/2007/app">

デフォルト名前空間も含めて、下記のような名前空間の辞書を作ります。

# ドキュメント XML の namespace
ns = {'def': 'http://www.w3.org/2005/Atom', 'app': 'http://www.w3.org/2007/app'}

ElementTree の find などを使うときは、この名前空間を指定してやります。例えば、 entry タグをとりたければ、

        self.__entries = root.findall('def:entry', ns)

のようにデフォルト名前空間も明示的に指定してやります。

これで、名前空間付きでタグとかが取れるようになりました。

(参考)

エントリーの取得処理

ここまでをまとめて、このデータ取得部分を含めたクラスを作って、イテレータでアクセスできるようにしました。

こんな感じですね。

# ドキュメント XML の namespace
ns = {'def': 'http://www.w3.org/2005/Atom', 'app': 'http://www.w3.org/2007/app'}

class HatenaEntries():
    '''
    はてなブログのエントリー(記事)を取得し、イテレータで扱うためのクラス
    '''
    def __init__(self):
        self.__isFirst = True
        self.__count = 0
        self.__index = None
        self.__entries = None

    def __iter__(self):
        return self

    def __next__(self):
        # 最初の呼び出し
        if self.__isFirst:
            self.__isFirst = False
            url = self.__getFirstpageUrl()
            self.__getCollectionFromHatena(url)

        if self.__entries is not None and self.__index == len(self.__entries) - 1:
            # 次のコレクションを設定
            self.__getCollectionFromHatena(self.__nextUrl)

        if self.__entries is None:
            raise StopIteration()

        self.__index += 1
        return self.__entries[self.__index]

    def __getCollectionFromHatena(self, url):
        if url is None:
            self.__entries = None
            self.__index = None
            self.__nextUrl = None
            return

        root = self.__getEntryCollection(url)
        self.__entries = root.findall('def:entry', ns)
        if self.__entries is None:
            self.__index = None
        else:
            self.__index = -1
            self.__count += len(self.__entries)
        self.__nextUrl = self.__getNextpageUrl(root)

    def __getFirstpageUrl(self):
        url = os.path.join(BASEURL, ENTRY_PATH)
        return url

    def __getNextpageUrl(self, root):
        for child in root.findall('def:link', ns):
            if child.get('rel') == 'next':
                return child.get('href')
        return None

    def __getEntryCollection(self, url):
        basic = base64.b64encode('{}:{}'.format(USER,PASSWORD).encode('utf-8'))
        headers={"Authorization": "Basic " + basic.decode('utf-8')}

        print("request url: {}".format(url))
        req = urllib.request.Request(url, headers=headers)
        try:
            with urllib.request.urlopen(req) as res:
                data = res.read()
        except urllib.error.HTTPError as err:
            print("exception occured to get entry collection");
            print(err)
            return None
        except urllib.error.URLError as err:
            print("failed connection to server");
            print(err)
            return None

        # XML に変換
        root = ET.fromstring(data)
        return root

    def getCurrentArticles(self):
        '''
        現在までに読み込んだエントリー数
        '''
        return self.__count

SyntaxHighlighter の記述の検出と言語の抽出

エントリーが取れるようになったので、次は正規表現で対象があるかをチェックします。

SyntaxHighlighter は

<pre class="brush: 言語名">

という記述をするのが基本なのですが、過去の経緯もあり、いくつかの表記パターンがありました。

<pre class="brush: 言語名">
<pre class="brush: 言語名" data-unlink="">
<pre class="brush: 言語名",  data-unlink="">
<pre class="brush: 言語名;">
<pre class='brush: 言語名'>

これらをカバーするような正規表現を作ってみます。

pattern = re.compile('(<\s*pre\s+class\s*=\s*(?P<quot>[\"\']?)\s*brush\s*:\s*([\d\w]+)\s*;?\s*(?P=quot)\s*,?\s*(?:data-unlink\s*=\s*(?:\"\"|\'\'))?\s*>)(.+?)(</\s*pre\s*>)', re.MULTILINE | re.DOTALL)

コード部分が複数行にわたるので、 MULTILINE と DOTALL を指定した処理にしています。 また、開始preタグ、言語名、コード本体、終了preタグをグループにして取得できるようにしています。 あとは、空白の有無とかクォーテーションの違いとか、各記号・data-unlink表記の有無に対応できるように作ってみました。

とはいえ、いきなりこの正規表現を作るのはたいへんでしたので、下記サイトなどで、チェックしながら作りました。

regex101.com

言語一覧の取得

上記で取得できるようになった各エントリーに対して、こんな感じでチェックしてやります。

import hatena_entries as HE


# 記事情報の取得
#   isLang, true: 言語一覧, false: 記事タイトル一覧
def listup(entry, isLang):
    global num_convert

    content = entry.find('def:content', HE.ns)
    title = entry.find('def:title', HE.ns)

    resultSet = set()
    # content に syntaxhighlighter の記述 があるか
    if content is not None and content.text is not None:
        m = pattern.findall(content.text)

        if len(m) > 0:
            num_convert += 1
            if isLang:
                for v in m:
                    resultSet.add(v[2]) # #3 lang
            else:
                resultSet.add(title.text)

    return resultSet

set(集合)を利用して、言語名の重複を除いてやります。

取得した言語名一覧と対応表

最終的に作成した言語名一覧と対応表は下記のような辞書にまとめて、変換処理で使うようにしました。

# 言語指定の変換一覧
#   key: SyntaxHighlighter
#   val: はてなブログ markdown
lang= {
    "actionscript3": "actionscript",
    "bash": "sh",
    "cpp": "cpp",
    "dart": "dart",
    "html": "html",
    "java": "java",
    "javascript": "javascript",
    "js": "javascript",
    "json": "javascript",
    "php": "php",
    "python": "python",
    "scala": "scala",
    "sql": "sql",
    "text": "",       # text には単なるコードブロックを対応させる
    "vb": "vb",
    "xml": "xml",
    "yaml": "yaml",
}

SyntaxHighlighter の記述を markdown へ変換

SyntaxHighlighter のタグさえ見つけられれば、変換はさほど難しくありません。

こんな感じで、変換関数を定義して変換します。

def replacer(matchObj):
    nl1 = "" if matchObj.group(4)[0] == "\n" else "\n"
    nl2 = "" if matchObj.group(4)[-1] == "\n" else "\n"
    # 問題再現
    #nl1 = ""
    #nl2 = ""

    return "```" + codeLang.lang[matchObj.group(3)] + nl1 + matchObj.group(4) + nl2 + "```"

def getConvertedContent(content_text):
    return re.sub(pattern, replacer, content_text)

実は、上記は正しく動作するのですが、当初は下記のように pre タグを置き換えるだけにしてしまいました。

def replacer(matchObj):
    return "```" + lang[matchObj.group(3)] + matchObj.group(4) + "```"

すると、開始preタグの後ろに改行なしでコード本体が書かれている場合に、言語名とコード本体がつながってしまうという事態になってしまいます。

この記述で問題ないと思い込んでしまったため、本番のブログへも反映してしまい、修正作業が大変になってしまいました。テストは大事ですね。 修正についても、後で述べます。

更新作業

更新はエントリーIDさえ、わかれば簡単です。

def updateEntry(entry_id, entry_xml):
    '''
    エントリーの更新
    '''
    url = os.path.join(BASEURL, ENTRY_PATH + '/' + entry_id)

    basic = base64.b64encode('{}:{}'.format(USER,PASSWORD).encode('utf-8'))
    headers={
        "Authorization": "Basic " + basic.decode('utf-8'),
        "Content-type": "application/xml",
    }

    print("request url: {}".format(url))
    req = urllib.request.Request(url, headers=headers, data=entry_xml.encode('utf-8'), method='PUT')
    try:
        with urllib.request.urlopen(req) as res:
            data = res.read()
    except urllib.error.HTTPError as err:
        print("exception occured for update, entry id: {}".format(entry_id));
        print(err)
        print("continue next entry")
    except urllib.error.URLError as err:
        print("failed connection to server");
        print(err)
        print("continue next entry")

なお、当初は method として小文字の put にしていたら、404 が返ってきて、ずいぶんと悩みました。大文字にしたら問題なく更新できました。

実行

スクリプトが完成したので、実行してみます。

まずは言語一覧を取得します。

mor@DESKTOP-H6IEJF9:~/work/hatena$ python3 convert_syntax.py -g
(略)
num of articles: 326
num of articles to convert: 164

actionscript3
bash
cpp
dart
html
java
javascript
js
json
php
python
scala
sql
text
vb
xml
yaml
mor@DESKTOP-H6IEJF9:~/work/hatena$

で得られた結果を Python コードに反映して、変換処理を行います。

mor@DESKTOP-H6IEJF9:~/work/hatena$ python3 convert_syntax.py -c
(略)
num of articles: 326
num of articles to convert: 164

mor@DESKTOP-H6IEJF9:~$

エラーがでなければ処理完了です。

変換結果

変換した結果は、例えば、こちらの記事のやつだと、

f:id:junichim:20201220002157p:plain

が、

f:id:junichim:20201223112500p:plain

のようになります。

コードハイライトの行番号とかは、カスタマイズ方法がネットにいろいろとあるので、また別途対応したいと思います。

確認と修正

変換した全ての記事を見るわけにはいかないので、いくつかの記事を調べて、取りこぼしや変換ミスがないかチェックします。

正規表現を下記のように変更して

pattern = re.compile('<\s*pre\s+class\s*=\s*[\"\']?\s*brush', re.MULTILINE | re.DOTALL)

タイトル一覧を取得して、差分をチェックします。

増えた記事があったら、変換漏れなので、対応を検討します。今回は、以前のブログよりインポートした記事が、編集モードは markdown なのに、中身は html で記述されていて、そのため、<pre class="brush: " タグを正しく検出できていないというものがありました。なので、これについては、記事そのものを直接修正しました。

言語名とコード本体が結合した問題

で、今回の変換でやらかしたのがこれです。

SyntaxHighliter の記述は html タグでの指定なので改行がなくてもいいことを完全に忘れてました。 仕方ないので、これの修正スクリプトも作りました。正規表現を

pattern_pre = re.compile('^(```)(.+)$', re.MULTILINE)
pattern_post = re.compile('^(.+)(```)$', re.MULTILINE)

のように開始部分と終了部分に対応できるように用意します。

変換処理は、開始部分と終了部分で処理を分け、開始部分はトリプルバッククォートに続く文字が、言語辞書に含まれているか文字か否かをチェックして、改行位置を切り替えています。

def pre_replacer(matchObj):
    langs = set([lng for lng in clang.lang.values() if len(lng) > 0])
    for lng in langs:
        if matchObj.group(2).startswith(lng):
            pre = matchObj.group(1) + lng
            content = matchObj.group(2)[len(lng):]
            if len(content) > 0:
                return pre + "\n" + content
            else:
                return pre
    return matchObj.group(1) + "\n" + matchObj.group(2)

def post_replacer(matchObj):
    return matchObj.group(1) + "\n" + matchObj.group(2)

def fixPreMarkdown(content_text):
    return re.sub(pattern_pre, pre_replacer, content_text)

def fixPostMarkdown(content_text):
    return re.sub(pattern_post, post_replacer, content_text)

これを、開始タグの修正->終了タグの修正と呼ぶことで、1行で開始タグと終了タグが含まれている場合にも対応できます。

これを実行して、なんとかおおむね修正できました。

html エスケープ文字の修正

SyntaxHighlighter v4 では pre タグで囲まれたコード本体に、 < や > などがあるときは html エスケープしないといけませんでした。 このままだと、markdown方式に変換すると、エスケープした文字がそのまま表示されてしまいます。

これについては、ちょっと面倒ですが、エスケープ文字を含むエントリー一覧を作成して、順次直しました。

これが一番大変でしたね。

br タグの修正

同様に、Syntaxhighlighter のコード本体に br タグが含まれる場合もあります。 これも同様に対象となるエントリー一覧を作成して、順次直しました。

まとめ

まだ、修正による不具合があるかもしれませんが、順次見つけ次第直すことにします。しかし、まあ、意外と修正するところが多くて、予想外に大変でした。変換時に改行コードを入れ忘れるというミスがなければもうちょっと楽だったかな?

似たような作業する方がいれば何かの参考になればと思います。

なお、完成したスクリプト全体は Gist に置いておきますので、ご興味のある方はご自由にお使いください(WTFPL ライセンスです)。

参考:既存のブログのコピーを作成

テスト用に、ブログの記事を一旦エクスポートして、インポートして作業も行いました。

この際、気づいたのですが、インポート後は、編集モードが『見たまま編集』になっています。前述したように、今のブログも別のところからインポートしてきましたが、編集モードはmarkdownです。どういうことだ?仕様が変わったのか?

と思い調べてみると、はてな公式のアナウンスはありませんが、どうも、

  • MovableType形式でインポート:みたまま編集モード
  • WordPress形式でインポート:markdown

となるようです。下記の事例の記事から推測しました。

ご参考まで。

追記

2020/12/28 追記

SyntaxHighlighter を使わなくなったので、こちらの記事で設定した、cssおよびjsファイルの読み込み設定(はてな側の設定)を削除しました。

Rainloop の upgrade, 1.13.0 -> 1.14.0

rainloop のバージョンを 1.13.0 から 1.14.0 にアップグレードしたので、手順をメモっておきます。

なお、公式の説明は、下記のとおりです。

Upgrade / Documentation /RainLoop Webmail

(必要があれば) EBS ボリュームのスナップショットを作成

現在、rainloop を EC2 で動かしているので、トラブル時に復旧できるよう、EBSボリュームのスナップショットを作成しておきます。

アップグレード作業

最悪の場合、元に戻せるようにバックアップを取っておきます。

ubuntu@ip:~$ mkdir tmp/rainloop_bak
ubuntu@ip:~$ cp -p -R /var/www/html/rainloop tmp/rainloop_bak

アップグレードしたいバージョンを取得します。

ubuntu@ip:~$ wget https://github.com/RainLoop/rainloop-webmail/releases/download/v1.14.0/rainloop-community-1.14.0.zip

解凍して

ubuntu@ip:~$ mkdir tmp/rainloop_1.14.0_test
ubuntu@ip:~$ cd tmp/rainloop_1.14.0_test
ubuntu@ip:~/tmp/rainloop_1.14.0_test$ unzip ../../rainloop-community-1.14.0.zip 

コピー

ubuntu@ip:~$ 
ubuntu@ip:~$ cd tmp/rainloop_1.14.0_test/
ubuntu@ip:~/tmp/rainloop_1.14.0_test$ 
ubuntu@ip:~/tmp/rainloop_1.14.0_test$ sudo cp -p -R ./* /var/www/html/rainloop/

所有者を修正

ubuntu@ip:~/tmp/rainloop_1.14.0_test$ cd /var/www/html/rainloop/
ubuntu@ip:/var/www/html/rainloop$ sudo chown -R www-data:www-data ./*

webからアクセスして、管理画面を表示すると、

f:id:junichim:20201217154658p:plain

バージョン番号が更新されているのがわかります。

簡単ですね。

設定ファイルのバックアップ

ついでに、設定ファイルのバックアップを取るようにします

こんなシェルスクリプトを ~/bin/rainloop_settings_backup.sh という名前で作っておきます。

#!/bin/bash
#
# rainloop の設定情報をバックアップ用にまとめる
#
# written by Junichi MORI, 2020/12/17

#
# rainloop's dir
#
RAINLOOP_DIR=/var/www/html/rainloop
TARGET_DIR=data/
# SETTINGS_DIR is a relative path from TARGET_DIR
SETTINGS_DIR=_data_/_default_
CACHE_DIR=${SETTINGS_DIR}/cache
LOGS_DIR=${SETTINGS_DIR}/logs

# アーカイブファイル名
ARCHIVE_PRFX=rainloop_settings
ARCHIVE_SUFX=".tgz"
ARCHIVE_FILE=${ARCHIVE_PRFX}_`date '+%Y%m%d'`${ARCHIVE_SUFX}

# 設定情報のバックアップ
echo "tar zcvf ${ARCHIVE_FILE} -C ${RAINLOOP_DIR} --exclude=${CACHE_DIR} --exclude=${LOGS_DIR} ${TARGET_DIR}"
tar zcvf ${ARCHIVE_FILE} -C ${RAINLOOP_DIR} --exclude=${CACHE_DIR} --exclude=${LOGS_DIR} ${TARGET_DIR}

rainloop のキャッシュディレクトリは、

data/_data_/_default_/cache
 

ログディレクトリは

data/_data_/_default_/logs
 

なので、これらを除外するようにしています。

※ キャッシュディレクトリの場所は、下記の issues のディスカッションで述べられていました

実行しておきます。

ubuntu@ip:~$ ./bin/rainloop_settings_backup.sh

できたファイルを保存しておけばOKです。