画像フィルタ付きFTPサーバを書いてみた


 Nexus 7で、電子化した漫画とか読むのにさー。あんまり大きいと、転送速度的に糞重くなるじゃないですか。
 でも、それを基準に画質落として保存するのもあれだし、わざわざ低画質版とかを分けて置いておくのも何となくアレで。

 で、オンザフライで画像を加工するFTPサーバなら、Python + pyftpdlib + ImageMagicK + Wandで割とすぐに書けそうな気がしたので、書いてみた。
 ついでに、削除したらゴミ箱に放り込む機能も欲しかったので、Send2Trashも使った。

(2014/7/10更新: 無害だと思ってたおまけ機能が稀にエラーを発生させていたので削除)
server.py:

from __future__ import division, print_function, absolute_import
import ConfigParser
import hashlib
import argparse
import sys
import os
import re
import Tkinter
from pyftpdlib.authorizers import DummyAuthorizer, AuthenticationFailed
from pyftpdlib.handlers import FTPHandler, DTPHandler
from pyftpdlib.servers import ThreadedFTPServer
from pyftpdlib.filesystems import AbstractedFS, FilesystemError
from send2trash import send2trash
from wand.image import Image
import wand.exceptions
from StringIO import StringIO



DEFAULT_CONF_FILE = os.path.splitext(__file__)[0] + '.ini'



class Conf(object):

    _parser = argparse.ArgumentParser()
    _parser.add_argument('--hash', help='generate hash, copy to clipboard and exit')
    _parser.add_argument('--conf', default=DEFAULT_CONF_FILE, help='specifies configuration file(.ini)')
    args = _parser.parse_args()

    def __init__(self):
        self._load_conf()

    def _load_conf(self):
        self._conf = ConfigParser.RawConfigParser()
        self._conf.read(self.args.conf)

    @property
    def conf(self):
        return self._conf

    @property
    def listen_address(self):
        return self.conf.get('FTPServer', 'Host'), self.conf.getint('FTPServer', 'Port')

    @property
    def image_target_width(self):
        return self.conf.getint('ImageFilter', 'TargetWidth')

    @property
    def image_target_height(self):
        return self.conf.getint('ImageFilter', 'TargetHeight')

    @property
    def image_shrink_limit(self):
        return self.conf.getint('ImageFilter', 'ShrinkLimit')

    @property
    def image_rotate(self):
        return {'none': 0, 'right': 90, 'left': 270}[self.conf.get('ImageFilter', 'Rotate')]

    @property
    def image_stretch(self):
        return self.conf.getboolean('ImageFilter', 'Stretch')

    @property
    def image_shrink(self):
        return self.conf.getboolean('ImageFilter', 'Shrink')

    @property
    def image_stretch_filter(self):
        return self.conf.get('ImageFilter', 'StretchFilter')

    @property
    def image_shrink_filter(self):
        return self.conf.get('ImageFilter', 'ShrinkFilter')

    @property
    def image_stretch_filter_blur(self):
        return self.conf.getfloat('ImageFilter', 'StretchFilterBlur')

    @property
    def image_shrink_filter_blur(self):
        return self.conf.getfloat('ImageFilter', 'ShrinkFilterBlur')

    @property
    def image_jpeg_quality(self):
        return self.conf.getint('ImageFilter', 'JPEGQuality')

    @property
    def image_trim(self):
        return self.conf.getboolean('ImageFilter', 'Trim')

    @property
    def image_trim_fuzz(self):
        return self.conf.getfloat('ImageFilter', 'TrimFuzz')

    @property
    def re_target_filename(self):
        return re.compile(self.conf.get('ImageFilter', 'TargetFileName'), re.I)

    @property
    def use_trash(self):
        return self.conf.getboolean('Global', 'UseTrash')

    @property
    def users(self):
        return self.conf.items('Users')

conf = Conf()



class DummyHashAuthorizer(DummyAuthorizer):

    def validate_authentication(self, username, password, handler):

        hash = hashlib.sha512(password).hexdigest()
        try:
            if self.user_table[username]['pwd'] != hash:
                raise KeyError
        except KeyError:
            raise AuthenticationFailed



class ModifiedAbstractedFS(AbstractedFS):

    def open(self, filename, mode):

        def call_super():
            return super(ModifiedAbstractedFS, self).open(filename, mode)

        if not conf.re_target_filename.search(filename):
            return call_super()

        try:
            with open(filename, mode) as fd, Image(file=fd) as image:
            # Image(filename=filename) doesn't handle unicode filename

                if conf.image_trim:
                    bg_color = image[0][0]
                    image.trim(fuzz=conf.image_trim_fuzz)
                    if image.width * image.height == 0:
                        image.blank(1, 1, background=bg_color)

                target_width = conf.image_target_width
                target_height = conf.image_target_height
                shrink_limit = conf.image_shrink_limit

                if (image.width - image.height) * (target_width - target_height) < 0:
                    # rotate
                    rotated_target_width = target_height
                    rotated_target_height = target_width
                    degree = conf.image_rotate
                else:
                    # not rotate
                    rotated_target_width = target_width
                    rotated_target_height = target_height
                    degree = 0

                # shrink first
                w, h = rotated_target_width, rotated_target_height
                if (conf.image_shrink and
                    (image.width > w or image.height > h) and
                    (image.width > shrink_limit and image.height > shrink_limit)
                    ):
                    if image.width / image.height > w / h:
                        h = image.height * w / image.width
                        if h < shrink_limit:
                            w = w * shrink_limit / h
                            h = shrink_limit
                    else:
                        w = image.width * h / image.height
                        if w < shrink_limit:
                            h = h * shrink_limit / w
                            w = shrink_limit

                    image.resize(width=int(w), height=int(h),
                                 filter=conf.image_shrink_filter,
                                 blur=conf.image_shrink_filter_blur)

                # rotate
                image.rotate(degree)

                # stretch last
                w, h = target_width, target_height
                if conf.image_stretch and (image.width < w and image.height < h):
                    if image.width / image.height > w / h:
                        h = image.height * w / image.width
                    else:
                        w = image.width * h / image.height

                    image.resize(width=int(w), height=int(h),
                                 filter=conf.image_stretch_filter,
                                 blur=conf.image_stretch_filter_blur)

                image.compression_quality = conf.image_jpeg_quality
                blob = image.make_blob(format='jpeg')
                io = StringIO(blob)
                io.name = filename

        except wand.exceptions.WandException:
            return call_super()

        return io

    def rmdir(self, path):
        if not conf.use_trash:
            return super(ModifiedAbstractedFS, self).rmdir(path)
        send2trash(path)

    def remove(self, path):
        if not conf.use_trash:
            return super(ModifiedAbstractedFS, self).remove(path)
        send2trash(path)



class FilteredImageFile(file):
    pass



def main():

    if conf.args.hash:
        hash = hashlib.sha512(conf.args.hash).hexdigest()
        print(hash)
        copy_to_clipboard(hash)
        sys.exit()

    start_server()



def start_server():

    handler = FTPHandler

    authorizer = DummyHashAuthorizer()
    for username, value in conf.users:
        homedir, perm, hash = value.split(',')
        authorizer.add_user(username, hash, homedir, perm=perm)
    handler.authorizer = authorizer

    fs = ModifiedAbstractedFS
    handler.abstracted_fs = fs

    server = ThreadedFTPServer(conf.listen_address, handler)

    server.serve_forever()



def copy_to_clipboard(value):
    Tkinter.Text().clipboard_clear()
    Tkinter.Text().clipboard_append(value)



if __name__ == '__main__':
    main()

server.ini

[Global]
UseTrash=true

[FTPServer]
Host=0.0.0.0
Port=21



[ImageFilter]

TargetFileName=\.(jpg|png|gif|bmp)$
JPEGQuality=90
Rotate=right
; left/right/none

TargetWidth=1200
TargetHeight=1776
ShrinkLimit=800

; Stretch/Shrink
;   Filter: http://docs.wand-py.org/en/0.3.5/wand/image.html#wand.image.FILTER_TYPES
;     Recommended Filters:
;       "undefined", "mitchell", "lagrange", "lanczos", "lanczossharp", "lanczos2", "lanczos2sharp"
;   FilterBlur:
;       > 1 is blurry, < 1 is sharp
Stretch=true
StretchFilter=mitchell
StretchFilterBlur=0.8
Shrink=true
ShrinkFilter=lanczossharp
ShrinkFilterBlur=0.8

; Trim Edges
;   TrimFuzz: how much tolerance is acceptable to consider two colors as the same
Trim=true
TrimFuzz=1



[Users]
;username=homedir,permissions,password_hash
;   permissions: https://code.google.com/p/pyftpdlib/wiki/Tutorial#2.2_-_Users
;     "e" = change directory (CWD, CDUP commands)
;     "l" = list files (LIST, NLST, STAT, MLSD, MLST, SIZE commands)
;     "r" = retrieve file from the server (RETR command)
;     "a" = append data to an existing file (APPE command)
;     "d" = delete file or directory (DELE, RMD commands)
;     "f" = rename file or directory (RNFR, RNTO commands)
;     "m" = create directory (MKD command)
;     "w" = store a file to the server (STOR, STOU commands)
;     "M" = change mode/permission (SITE CHMOD command)
;   password_hash:
;     generate with '--hash' option and paste it(copied to clipboard automatically)
reader=D:\foo\bar,elr,ba3253876aed6bc22d4a6ff53d8406c6ad864195ed144ab5c87621b6c233b548baeae6956df346ec8c17f5ea10f35ee3cbc514797ed7ddd3145464e2a0bab413
user=D:\foo\bar,elrdfmM,b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86

 こんな感じで。なかなかのやっつけ感。つーかこれだけで変態FTPサーバが動いちゃうんだもんなー。
 こういうのが欲しい人には非常に便利な気がするんだけど、人に使ってもらう気の無さすぎる記事である。説明めんどくて。コマンドラインに-h付けて起動すれば、あとは勘で何とか…、ならないかなあ。誰かもっと普通に扱いやすいソフトとして作る人いないだろうか。既にあったら、または作ったら教えてください是非。こんなもん捨ててそっち使いますし。
 サンプルがファイル書き込みだけ禁止してるのは、例えばファイルコピーをしようとするとフィルタ済み画像をコピーしちゃうことになるんで、このサーバの動作を考えると危険だな、と。コピーじゃなく移動なら大丈夫だけど。
 あと、サンプルのreaderのパスワードは123456、userのパスワードはpasswordである。最悪パスワードの新旧王者であるな。

 で、ESファイルエクスプローラーでアクセスしてみたんだけど、以前からFTPでの画像ロードに怪しさがあったのが再発して、どうも発生条件が分からんし普通のFTP鯖でも起きるんで、Rhythm SoftwareのFile Managerを使ってみているところ。この変態FTPサーバで使う以外の用途だと微妙だけど、それなりには行ける。ESファイルエクスプローラーと併用するかな。
 Androidのファイルマネージャーってどうも一長一短な感じなんで、結構なチャンスだと思うんだけど、誰か完璧な奴を作ってくれんかなー。

 せっかくなので、コードについての話も書いておく。ここは備忘録でもあるので。

  • ThreadedFTPServerを採用したのは、低速なファイルシステムに近い状態になっていた為。つってもI/OじゃなくCPUなので、Pythonだとマルチスレッドよりマルチプロセス向けなのではないか、と思ったけど、マルチプロセス版のクラスはWindows対応ではないっぽいので。クラス名を一箇所書き換えるだけでマルチプロセス版になるけど。
  • 画像変換のところは同じような処理を繰り返してて汚いけど、とりあえず動かすだけで疲れた。綺麗にするのめんどい。
  • ConfigParser関連ももっとすっきりやれそうな。
  • どうせやっつけなら設定ファイルをわざわざ分けない方がすっきりしたのに、と今では思う。でもまあ、これのお陰で、サーバを動かしたまま別PCから設定だけ変更する、というのが簡単にやれるからいいか。←エンバグしてたので機能削除

 改善案とかのツッコミは歓迎である。


さくらのVPSでカーネル更新したら起動しないし


 このblogとか置いてる鯖は、さくらのVPSでCentOS6が入ってるんだけどさ。
 昨日辺りにyumでカーネル更新したら、起動しなくなった。ひゃー。VPSでカーネルパニックは初めて食らったかも。

 理由はこいつで、要するに2.6.32-431.1.2.el6.x86_64だと、

FATAL: Error inserting scsi_wait_scan (/lib/modules/2.6.32-431.1.2.el6.x86_64/kernel/drivers/scsi/scsi_wait_scan.ko): Required key not available

とか連続で吐いて止まる、と。ふむ。
 で、起動させるには、さくらのVPSのリモートコンソールならOS起動前でも遠隔操作出来るので、再起動して即座にリモートコンソールに移動して、キー待ちのメッセージが出てる間にEnter、もう一度キー待ちメッセージ中にEnter、…だよな?(適当にやってるので記憶が曖昧)
 で、Kernelの選択メニューが出るので、一個前のバージョンっぽい名前のカーネルを選択してEnter、で無事起動。こえーこえー。
 一応、/boot/grub/grub.confdefault=0default=1にしといて、修正待ちかなー。

 と思ってたらさすがに即座に修正(2.6.32-431.1.2.0.1.el6.x86_64)が来たのでyum update。再起動で問題無しを確認。やれやれだぜ。

 というのは半分嘘で、カーネルとは関係無い話だけど、再起動したらfail2banに自宅IPアドレスを引っ掛けられてSSH不能というアホなミスをやらかしていた。リモートコンソールでfail2banを止めて、真面目にjail.confignoreipにDDNSな名前を突っ込んでおいたのでもう大丈夫。
 DDNSは本気で欲しくなったら自前鯖で、とか思ってたけど、別に適当な外部サービスで良かったっぽい。CNAMEで自分とこの名前付けて、基本的にそっちの名前を使うようにしていれば、移転の時も困らないだろうし。

 ということで、OS起動前から使えるリモートコンソールは必須だろー、という話。管理者がアホな場合は特にね!


QNAP TS-121とNexus7でVPNしてみる


 QNAPのTurboNASシリーズは、所有者専用の無料DDNSサービスもあるし、OpenVPNやPPTPのサーバもポチポチとクリックするだけでちゃっちゃと立てられる。らしい。筈。
 電子化した漫画を外出先でちょこっと表示したり、リモートデスクトップでWindowsとか触れたりすると、まー助かる時もあるかもしれないし、ネタ的にも面白いよなー、ということで、やってみた。
 PPTPは、脆弱性が色々と言われてるし、そもそもMD4とかDESとかRC4(128bit)とかの名前が出てくるのが限りなくノーガードに近いアレ感なので、OpenVPNが駄目だった時に考えよう。

 つーことで、まずはOpenVPNから。
 TurboNASのファームは4.0.2なので、3系とは違うこともあるかも。

 最初はDDNSを設定してしまうことにする。
 QNAPの所有者用のDDNSはmyQNAPcloudって奴で、これはもう何も読まずに適当にやっても行けるレベルの簡単さ。NASのいつもの偽デスクトップみたいな設定画面からざくざく登録と設定。
 IDがメールアドレスなのは、どこにも書いてないんで不親切だけど。あと、DNSに登録される名前は、後からでも好きに変えられそうな雰囲気だった。試してないけど。

 で、myQNAPcloudを設定しただけで、何やら勝手に色々なサービスが有効になる。やめろー。
 とりあえず、OpenVPNとMy DDNSだけ有効にして、他のサービスは全部切る。Cloud Portalも何のサービスだか分からんけど、全部「公開」をオフに。
 自動ルータ構成は、有効にした方が楽なんだろうけど、ルータのuPnPは無効にしておきたいのだよな。だってLAN側の機器からポートマッピングが設定出来るんだぜ、認証とか無しで。怖いじゃないか。某事務所が同じLANにいるから、何かあったら困るのだ。VLANで一応の隔離はしてるけど。
 一般的にはuPnP有効でも大丈夫な気もするけど、一応uPnP無効で進めたいので、ルータのポートマッピング設定をする。OpenVPNはポート一個で全部済むから簡単である。いわゆる普通のポート開放と全部同じ手順。その都合で、NASも固定IPアドレスに変更した。ルータのポートマッピングがIPアドレス指定だからだけど、AndroidのOpenVPNだと普通TUNらしいからNetBIOS名の解決も無理だろう。NASのOpenVPNサーバ設定にWINS関係も見当たらんしな。
 そのOpenVPNサーバ設定は、暗号化をAES(256bit)に何となく変えてみた程度。あとは「設定ファイルのダウンロード」を押せば、ca.crtopenvpn.ovpnの入ったzipが落とせるから、これをクライアントにインポートすれば、クライアントの設定も簡単に終わってめでたしめでたし、って寸法らしいぜー。
 そうは行かなかったが。

 サーバ側の設定が終わったので、今度はAndroid(4.4)のクライアントを入れる。良く分からんけど、公式(?)クライアントにしといた。OpenVPN Connectって奴。
 さっき落としたca.crtopenvpn.ovpnを、Nexus7の適当なローカルフォルダに置いて、Import > Import Profile from SD cardで取り込んで、ユーザー名とパスワードはとりあえずNASでVPNアクセスが許可されてるユーザーのソレを突っ込めばいい、と思われたのだが、この手順だと「証明書が見つかりません」とか言われて進めなくなった。
 対策は色々あるのかもだけど、以下の手順で予めopenvpn.ovpnを書き換える方法がフォーラムに上がっていた。

 まず、ca.crtの中身は

-----BEGIN CERTIFICATE-----
へんだらもんだら
ほんがらふんがら
-----END CERTIFICATE-----

のようになっていると思うんだけど、そいつをコピーする。
 次に、openvpn.ovpn

ca ca.crt

という行を削除して、代わりに

<ca>
-----BEGIN CERTIFICATE-----
へんだらもんだら
ほんがらふんがら
-----END CERTIFICATE-----
</ca>
setenv CLIENT_CERT 0

って感じで、さっきコピーした奴を貼り付けて、前後にちょっとおまけを付け足す。
 あとは、openvpn.ovpnImport Profile from SD cardで取り込んで、正しいユーザー名とパスワードを入れれば終わり。
 ちなみに、VPNを使えるユーザーの設定は、コントロールパネル→アプリケーション→VPNサービス→VPNクライアント管理とかで行ける。

 でもこれ、多分ca.crtを埋め込む作業は不要で、単に

setenv CLIENT_CERT 0

の一行を追加すればいいだけな気がする。やり直すの面倒だから試さないけど。ca.crtの埋め込みは何となくiPhone向けの手順という気もしなくもないけど、iPhoneは全く分からんのであった。

 最後にLTEから自宅LANへの接続テストをしてみたけど、NASのファイルも見えるし、Splashtop2も無料のまま繋がったので、成功したっぽい。めでたしめでたし。Importに使ったファイルは漏洩するとまずいのでさっさと消去。
 あとは、OpenVPN ConnectのPreferencesを適当に一通り見て回って、Battery SaverをONにしてみたり。
 それと、何かの拍子に左上にアイコンが残りっぱなしになった時は、Exit Allで終わらせられるっぽい。

 ただ、Android4.4の仕様なのか、「ネットワークが監視される場合があります」という警告が出っぱなしになるのがうざい。英語メッセージだと"Network may be monitored"なので、そっちで検索すると色々なやり取りが見付かるんだけど、結局rootedじゃないと消せないらしい。
 こういう、狼少年の警告とでも言うべき仕様にされると、本当に警告が必要なことが起きても気付かなくなるのだよな。直して欲しいなあ。


QNAP TS-121でsmartmontoolsを使う


 久々に技術寄りの記事であるな。

 QNAP TS-121に、外付けHDDケース(WD Red 4TB入り)をeSATAで接続して、Q-RAID1とかいう怪しいブツでミラーリングを始めたのであるが。
 どうも外部接続ドライブに対するSMARTの監視方法が見当たらないのだ。それ困るじゃん?
 SMARTで寿命の予測なんか出来ないけど、壊れた時にSMART監視ソフトで気付くということは結構あるからなあ。

 つーかQ-RAID1とかいう変なのを使うくらいなら素直に2ベイのQNAP買ってRAID1で良かったんじゃね、って思う人がそろそろ多数かもしんないけど、「RAID1で二台まとめて死にました」の記憶が哀しすぎるので、まだまだ我が道を行くのである。普通は2ベイの方がいい気もしますが。
 元々、Q-RAID1なんて仕様不明のブツ使わんで、QNAPもう一台買ってホットスタンバイだぜー、とか思ってたんだけどさ。高いので。ここは過渡的な環境ってことで一つ。使ってる人が少なそうな機能に依存するのは危険な気もするのだが。

 まあ、ベイの数はどうでもいいや。本題に進もう。

 QNAP使ってたら、smartmontoolsを使いたい人も多いと思うんですよ。CrystalDiskInfoみたいに、代替処理が入ったら即警告するレベルならいいんだけど、QNAPの報告機能がどんな基準なのか分からんしさー。SMARTの標準だとすげー緩い基準だし。
 なので、ipkg版smartmontoolsを試してみたけど、

[~] # ipkg install smartmontools
Installing smartmontools (5.40-3) to root...
Downloading http://ipkg.nslu2-linux.org/feeds/optware/cs08q1armel/cross/unstable/smartmontools_5.40-3_arm.ipk
Configuring smartmontools

#################################################################
#
#   To automatically start smartd on bootup, edit
#
#     /opt/etc/init.d/S20smartmontools
#
#   and follow the instructions in this file. smartd can be
#   configured in /opt/etc/smartd.conf
#
#################################################################

Successfully terminated.
[~] # smartctl -i /dev/sda3
smartctl 5.40 2010-10-16 r3189 [arm-none-linux-gnueabi] (local build)
Copyright (C) 2002-10 by Bruce Allen, http://smartmontools.sourceforge.net

Segmentation fault
[~] # ipkg remove smartmontools
Removing package smartmontools from root...
smartd: no process found
Successfully terminated.
[~] #

こんな感じでセグフォするので消した。
 原因はどうやら、smartmontoolsが5.39でC++の新しい機能を使うようになって、GCC 4.2.3がARMだとダイヤモンド継承絡みの最適化でバグるとか何とか。でもダイヤモンド継承は必要だから使ってるらしい。GCCを-fno-toplevel-reorderで走らせれば動くぜ、と。なるほど。このipkg版の存在する意味が分からんな。
 つまりあれだな、QNAPでGCC使えるようにしないと駄目か。めんどくせえ予感。Debian化しちゃう人の気持ちが少し分かる気もした。

 調べてるうちに、QNAPにデフォで入ってる/sbin/nasutilにsymlinkしてるget_hd_smartinfoでSMART情報が取れる、と判明。

[~] # get_hd_smartinfo
Usage: get_hd_smartinfo [OPTION]...
Print HardDisk SMART Information.

        -h, Display this help and exit.
        -d, NUM Set disk number. (NUM > 0)
        -i, Timer Interval.(sec)
        -o, When -i exist,save to output File.(default save to /tmp/sd[a-g]_smart_xxxx)
        -m, When -i exist,mask attribute ID number(e.g -m 194,131)
[~] # get_hd_smartinfo -d 1
001 Raw_Read_Error_Rate          0        200 200 051 OK
003 Spin_Up_Time                 6008     219 219 021 OK
004 Start_Stop_Count             7        100 100 000 OK
005 Reallocated_Sector_Ct        0        200 200 140 OK
007 Seek_Error_Rate              0        200 200 000 OK
009 Power_On_Hours               334      100 100 000 OK
010 Spin_Retry_Count             0        100 253 000 OK
011 Calibration_Retry_Count      0        100 253 000 OK
012 Power_Cycle_Count            2        100 100 000 OK
192 Power-Off_Retract_Count      1        200 200 000 OK
193 Load_Cycle_Count             2509     200 200 000 OK
194 Temperature_Celsius          37       115 113 000 OK
196 Reallocated_Event_Count      0        200 200 000 OK
197 Current_Pending_Sector       0        200 200 000 OK
198 Offline_Uncorrectable        0        100 253 000 OK
199 UDMA_CRC_Error_Count         0        200 200 000 OK
200 Multi_Zone_Error_Rate        0        100 253 000 OK
[~] #

なるほど。193のLoad_Cycle_Countが凄いけど気にしないぜ。005と196と197と198が0ならいいのよ。

 こいつにPythonでも組み合わせれば自動監視も楽勝かなーと思ったけど、eSATAとかUSBとかに接続したドライブを指定出来ない模様。外部ドライブにも対応してくれよーとか、外部ドライブのSMART情報取る方法どこにも無いんだけどーとか、フォーラムで要望出した人はいたんだけど、中の人に「NASに何を求めてるんすか」みたいな感じで切り捨てられてた。うむ。QNAP買ったの間違いだったろうか、という気分に少しなったな。安いPC買った方が良かった説も。
 その要望のところに、smartmontoolsでも取れなかった、みたいに書いてあるのが不吉だけど、まあ試してみよう。

 えーと、QNAPでsmartmontoolsを動かせた人の話は、と。Projekt Qnap(追記:多分閉鎖?)ってとこにあるな。
 割とそのまま素直な内容に見えるので、必要最小限っぽいところを切り出して軽くアレンジ。

cd /tmp
ipkg install make
ipkg install gcc
ipkg install grep
ipkg install gawk
ipkg install optware-devel
export PATH=/opt/bin:$PATH
wget 最新版URL
# http://sourceforge.net/projects/smartmontools/files/ の中に最新版がある筈
tar zxf smartmontools-バージョン番号.tar.gz
cd smartmontools-バージョン番号
./configure CXXFLAGS='-g -O2 -fno-toplevel-reorder -Wall -W'
make
make install
update-smart-drivedb

ってとこか。ipkg installは一行にまとめられそうな気もするけど。
 なお、この記事を書いた時点では、smartmontoolsは6.2が最新版である。

 んで、インストール実行。
 あっさり完了。

 んじゃー走らすか。dfした感じだと、eSATAは/dev/sdzみたいだから、

[~] # smartctl -d marvell -a /dev/sdz

と。-d marvellは重要。(追記:QTSとsmartmontoolsを両方更新したら、/dev/sdz/dev/sdza-d marvell-d satにそれぞれ変更しないと駄目になった)
 で、行けた。ちゃんと情報表示されてるじゃーん。良かった良かった。
 でもさー、こんくらいnasutilで対応させらんないのかね。いいけどさ。

 次は、監視させる訳だが、smartdで行けるかどうか。
 監視基準は、CrystalDiskInfoと同等でいいや。監視対象は/dev/sda/dev/sdzの両方にしよう。となると、どうすればいいのかな。えーと。
 QNAPで任意のアプリケーションを自動的に起動させる方法から調べてみようか。QNAPedia Community Wikiの記事を参考に、なるべく素直に真似する感じで。

 今のファームでも通用するやり方は、QPKGでパッケージがインストールされてるフリをする方法っぽい。
 サンプルを真似して、/etc/config/qpkg.confの下の方に、以下の内容を追加してみる。

[autorunSmartmontools]
Name = Autorun Smartmontools
Version = 6.2
Author = autorun
Date = 2013-12-03
Shell = /share/HDA_DATA/.qpkg/autorun/smartd.sh
Install_Path = /share/HDA_DATA/.qpkg/autorun
QPKG_File = autorun_smartmontools.qpkg
Enable = TRUE

ブラウザでApp Centerを開くと、おー、マイアプリに登録されてる。あとはこの内容通りに/share/HDA_DATA/.qpkg/autorunディレクトリを作成して、その中に必要なファイル一式を作れば良さそうだな。
 なお、このシステムはRAMディスクにマウントされてる場所が非常に多いので、HDD上に実体のあるディレクトリに置かないと再起動で何も無かったことになる。TS-121だと/share/HDA_DATAが良さげだけど、機種によって場所は変わる模様。
 あと、QPKG_Fileの指定は、どうせ存在しないので適当な名前にした。平気かどうか知らんけど。

 上のShellってとこに指定したのが起動スクリプトらしいので、今度はそのsmartd.shを書く。

#!/bin/sh
/usr/local/sbin/smartd -c /share/HDA_DATA/.qpkg/autorun/smartd.conf --interval=600 -p /var/run/smartd.pid

ってとこか。
 インターバルは、まあ、デフォの30分でいい気もするけど、何となく10分に。別に固まったりもしないようだし、1分くらいでもいいかもなあ。
 smartd.confは、デフォの/usr/local/etc/smartd.confは変更せず、起動スクリプトと同じ場所に一から作ることに。
 PIDファイルは、まあ、あって困ることも無いから指定しておく。

 次は、smartd.conf。飽きてきたが適当に頑張ろう。

DEFAULT -d marvell -n standby,q -a -R 5! -s S/../.././02 -s L/../01/./08 -m foo@example.com -M exec /share/HDA_DATA/.qpkg/autorun/smartd-mailer.sh
/dev/sda
/dev/sdz

こんな感じかねー。
 -n standby,qは、スタンバイモードで止まってる時にSMART読みでスピンアップさせない為に指定。まあ、スピンダウンしない設定にしてる筈だけど一応。
 -R 5!は、代替セクタが出た時に致命的と判定させたいので指定。
 まあ、何かやたらと色々指定出来るので便利そうだ。後で不都合なことがあったらまた設定を弄ってみよう。

 と、説明を端折ったけれども、最後の-Mの指定は説明せんとくん。
 これは、smartdのエラー報告をメールでやらせたい時に、どうやって送るかを指定するオプションなんだけど、無指定だとmail経由で送るらしい。QNAPには存在しません。うむ。
 QNAPでスクリプトからメールを送る方法をフォーラムで探すと、sendmail -tを使ったら行けた、とのこと。一方、送信者であるsmartdの仕様は、mailが駄目な場合はスクリプトに渡すのが一番楽っぽい。
 以上により、-M exec /share/HDA_DATA/.qpkg/autorun/smartd-mailer.shという指定が入っているのであった。

 ということで、smartd-mailer.shを書こう。

#!/bin/sh
{ echo -n -e "To: $SMARTD_ADDRESS\nSubject: $SMARTD_SUBJECT\n\n"; cat; } | /usr/sbin/sendmail -t

これでどうだ。
 んで、忘れずにまとめてchmod u+x *.shをしておく。
 最後に、メールテストの為にsmartd.confの一行目の末尾に-M testを追加して、

[~] # /share/HDA_DATA/.qpkg/autorun/smartd.sh

で起動。よし、テストメールが正常に届いた。メールクライアント側で迷惑メールに入らないように設定して(重要)、さっき追加した-M testを消して、起動し直して、終わりー。うへー。疲れたわー。
 でも、実際にSMARTエラーが発生した時にちゃんと動くのか不明。一見動きそうに見えるけど、「これじゃ動かねーよバーカ」とかあったら是非コメントを。

 そして、Q-RAID1とやらのミラーリングは糞遅かった。eSATAなんだし、もーちょい何とかならんのかな。psで見る限り、rsync 3.0.7を改造して動いてるっぽい雰囲気ではあるけど、何でこんなに遅いんだろう。
 いつまでも終わらないし、終わるまでは再起動もしたくないんで、smartdが自動で起動するかのテストも出来ないのだが。失敗したら報告の記事書いて、ここに追記もすればいいか。
 Q-RAID1がハードリンクをちゃんと処理するかとかも確認するつもりだったけど、こっちは特に異様なことが起きなければ報告の記事は書かなくて良さそうかなー。

追記:
 Q-RAID1はハードリンクを理解しなかった。フォーラムでも六年以上前に「-H付けて」「検討しとくわ」ってやり取りがあったんで、分かってるけど放置っぽい。これも「所詮NASですから」ってことかね。
 まあ、データ置き場にハードリンクなんて普通使わないだろ、って言われればそれまでだが。世代管理出来ないなーどうしよう。

追記2:
 再起動したらsmartdは自動起動しなかった。ぐぬぬ。Q-RAID1は頭から同期し直しだけど10分くらいで完了。まあただのローカルrsyncみたいだしな。
 自動起動は、調べても今一つ良く分からんし、もうめんどくせーから多重起動対策した起動スクリプトをcronで毎時間起動とかで誤魔化すかなー。

追記3:
 ファームを更新(QTS4.0.6→4.1.0)したらsmartmontoolsのtarball取ってくるとこからやり直しになった。毎回必要かは分からんけど。若干めんどくせー。インストール先をHDA_DATAの中にした方が良かったかも?
 で、smartmontoolsも6.2→6.3.1になって、QTSとsmartmontoolsのどっちのせいか分からんけどややこしい問題多発。対応した設定は本文中に追記。
 更にVLANとかGmailセキュリティ強化とかの都合でうまくメールも届かず、面倒なのでPythonでemailとsmtplibを使ってsmartd-mailer.pyとか書いて解決。SMTP認証をスクリプトで書くと何か気持ち悪いが。実に色々と面倒であった。
 つーことで、元から不親切な記事だったけど、いよいよ使いにくい記事になった。俺だって「これコピペすりゃ大体使えるぜー」って記事にしたい気はするんだけども何だかだるくてもー。


NASへのミラーリングに必死の巻


 さあ、QNAP TS-121が届きましたよー。NASですよNAS。わーい。
 って今更盛り上がるもんでもないが、自分用のNASって実は初めてだったのだよ。自分でもびっくり。よそのNASばかり面倒見てるからだな。

 QNAPの導入は簡単だ。トレイを取り出し、同梱のインチネジの方でHDDをトレイに留め、トレイを突っ込み、ケーブル全部繋いで電源投入。起動したらQfinderからブラウザの設定画面に飛んで、初期化開始。フォーマットが入るんで割と待たされるぜ。
 終わったら、かなーり適当に一通り設定を見て回る。迷うようなとこもほぼ無い。
 HDDの電源設定は、うーん、回しっぱなしかな。使い勝手良し、故障率低下説も有力で、消費電力増だけがデメリットだけど、今回使うWD Redはアイドル4Wくらいなので、故障した時のコストを考えれば安い保険だ。

 さて、なるべく安くて飛びにくいデータ保管庫を作ろう。うまく行くかな。

 今回は、ミラーリングとロールバックの切り分けを、今までより露骨にやってみる。
 ミラーリングは自動でリアルタイムに。RAID1は二重化と言うには密接過ぎるので、もう少しだけでも分散出来る方法で。
 ロールバックは、ミラーリングの「破損もそのまま伝播」への対応なので、rsyncにlink-destオプション付けて、NASローカルで世代管理すれば行けるかな。ハードリンクだと冗長性が無いけど、使い勝手が実にいいので、冗長化はミラーリングに任せる。
 バージョン管理はしない。それが必要なら別階層で。

 何か綱渡りな感じだな。どっかで全てにデータ破損が伝播するケースがありそうな無さそうな…。
 単純な操作ミスとかは大丈夫。タイムスタンプが一致しなければ世代管理対象になる。世代管理はリアルタイムじゃないから、スケジュールの合間の取りこぼしはあるけど、これの回避は当面は諦めてる。全部バージョニングする世界って一般人でも扱えるんだろか。
 プライマリのFS階層外でのデータ破損があっても、ミラーリングされるまでは伝播しないし、ミラーリングされるのはタイムスタンプが変わった時だけだから、世代管理に引っ掛かってセーフ、な筈だよな。うん。
 まあ、いいや。真の100%はどうせ無理なのだ。真の100%に近付くには、Amazon S3の海外拠点とかに流さないと。Glacierでも相当なお値段です。ネットワークも遅いです。あと、アレな画像とかは家の外に出したくないです(笑)。

 つーことで、この方針で。

 まず重要なのは、WindowsからNASにリアルタイムでミラーリングするソフトだなー。
 NetBak Replicatorの表示だと、ファイル数は約92万、サイズは420GB程度。他に管理してるSOHOとかのデータより遥かに多い感じ。さてどうなるか。しかもこれ、今後さらにどんどん増えるのだ。その為に整備してるんだしな。

 結論から言うと、最初に使ってみたNetBak Replicatorは、どうやら微妙。
 第二候補だったBvckupは、まあ、紆余曲折はあったけど、行けそうな雰囲気。大量ファイルならv2推奨。ただしv2は有料化の予定。

 NetBak Replicatorの最大の問題は、遅いこと。
 起動する度にコピー先の全ファイル情報を見に行くので、ファイル数が多いと異様な時間が掛かる。途中で放置して寝たけど、6時間とかじゃ全然足りなかったような。12時間とか行ってたかも。PCが再起動すればチェックし直しである。ここさえ飛ばせれば、致命的な不満は無いんだけどなあ。
 他の問題は、日本語訳がおかしいので軽く一瞬迷うこととか。まーこれは何とかなるけど。

 処理出来ないファイル名も幾つかあった。
 まず、foobar~1.txtのようなファイル名。ロングファイルネームなのに8.3形式の例のアレみたいな形にちょうどなっているのが呪われているのか何なのか、処理に失敗していた。
 もう一つは、フルパスが長すぎる場合。大抵はファイル名が長すぎるケースだけど。これ引っ掛かるソフト多いけど、バックアップソフトはちゃんと対応して欲しい。本気出せばWindowsのフルパスは64k文字くらい行ける筈なんで。(追記:そもそもNAS側からして対応していなかったというオチであった)
 ジャンクションとかは試さなかった。まー、普通は気にしなくていいかな。

 んで、次にBvckup導入。
 v1とv2があって、どっちもβ。正式版は有料予定。ただしv1は永遠のβのまま開発停止っぽい感じ。んー、まずはv1で試すかな。$20の価値を感じたらv2を試そう。

 NetBak Replicatorが処理出来なかった変なファイル名は、Flexible Renamerと手作業でリネーム済みなので、同じ問題が起きるかどうかは不明。
 ただ、長いフルパスの問題は、v1では出るっぽい。v2で修正した、とかフォーラムに書いてある。うーむ。まー仕方ないか。
 他にもフォーラム見てると、v2で修正したという話がちょこちょこある。特に、v1で大量のファイルを食わしたらハングした、というのは非常に困る可能性が。たかだか10万ちょいで起きたとか何とか。
 どうやら、ファイル内の差分コピーの為にCRCを細分して取ってるらしく、メモリを馬鹿食いして落ちるんじゃないか、みたいな話になってた。ファイルを丸ごとコピーするモードにしとけばマシっぽい。英語フォーラムの斜め読みだから怪しいけど。
 子供の頃にネットあったら、英語の勉強とかすげーやる気違っただろうなー。ネットに多少突っ込むと必ず英語にぶち当たるしさ。最近、若い世代の英語の成績も上がってるらしいっすね。そりゃネット見てれば英語勉強する気も全然違うよなあ。
 まあ、ファイルを丸ごとコピーするモードに設定しとく。メモリ食い過ぎは困るし、バグを踏む可能性も格段に減るしな。巨大ファイルに追記を繰り返したりすると頻繁なコピーで死にそうに思えるけど、同じファイルを繰り返しコピーしない時間を設定出来るから、そこを1分くらいにしてみる。
 ジャンクションとかへの対応もv2っぽい。まあ使わないけど。

 起動してみると、快適そうな雰囲気ではある。非常にシンプルに扱えるけど、Googleみたいに「シンプル過ぎて時々困る」までは行かない。設定画面を潜ってくと、ツボを突いた設定もちゃんとある。
 既にコピー先にあるファイルを残したまま同期しようとすると、英語でダバダバ警告されるので、NetBak Replicatorがコピーした物は使わず、別のコピー先で最初から同期し直し。まあ、速度の比較にもなるだろう。
 さすがに初回は全部コピーするだけだから、速度はあまり変わらん感じ。NetBak Replicatorは48時間ほど掛かったが、さてどうなるか。
 数時間ほど放置してから、ログ表示モードをエラーログのみにして、また全てのログを表示させようとしたら、UIが無反応に。ハングはしてないっぽいんだけど、プロセスがCPUを50%食ったまま「反応なし」表示。あー、ログが大量過ぎて、レンダリングで固まったんだろか。1ファイルごとに2行のログが出てたもんなー。やっぱ不安だなあv1。

 この辺で、v1をまだ試すか迷ったけど、結局こいつともう少し付き合ってみることに。強制終了してやり直そう。
 ここまでにコピーされたファイルは全廃棄になるけど、エクスプローラーで普通に削除しようとしたら「残り一時間」とか出た。ぐぬぬ。残したままでも行けるかもだけど、出来れば更地にしておきたい。いい手無いかなー、と思ってふと気付いた。シェルからrmすればいいんじゃね?
 早速sshでrm -rf /share/共有名/パスを実行したら、数十秒で完了。うむ。

 40時間くらい放置してから見に行くと、激しく変な名前のファイルの処理に失敗して、リトライを繰り返していた。文字化けみたいなファイル名だな。どっかのWebからWgetでミラーリングした奴か。GETメソッドのQuery Stringをそのままファイル名にするから、たまに謎ファイル名になるんだ。何か変な処理したんかね。不安ですね。エラーログを吐くだけマシかもだけど。
 問題のファイルは、要らないことを確認した上で消したけど、ここでv1を諦めることに。

 うーん。v2もどうなんだろなー。
 でも有料になるっぽいから、まずはフリーソフトをもう少し探してみるか。

 監視型ミラーリングのソフトも結構あるもので、Lazulite、ばくあぷ、御鏡、robocopyなどを検討してみたけど、どれも何かに引っ掛かって脱落。何でだったかもう忘れた。
 Bvckup2、試すかー。

 インストールすると、v1よりシンプルさが少し減ったけど、高機能な雰囲気。高機能になれば多少オプションとかが増えるのは仕方無かろう。英語をちゃんと読まないといけない部分が格段に増えてるのは、うーん。多言語対応とかしないかなー。
 まあでも、プログラマー的には余裕で読めるので、ちゃっちゃと設定していく。が、"What to backup"の選択ダイアログを開きに行ったら、また固まった。んんー、重い処理してそうなとこあるかな。フォルダのツリー探索とかしてるんかなー。
 と思いつつ、しばらく待ってみたら動いた。大量のファイルやフォルダがある時の挙動が甘いのかな。αテストではここまで大量のフォルダが無くて、βテストに問題が漏れてくるのかもなあ。

 でも、設定終了後、初回コピーが始まった途端、細かいうだうだが吹っ飛んだ。くっそ速え。
 大量の小さいファイルをネットワーク越しにコピーしようとすると、普通どうしても遅くなるんだろうなー、と思ってたけど、こんなに速く行けるのか。テンション上がるなこれ。$20も許せるかもしんない。
 まあ、大量コピーの速度が役に立つのは初回だけかもしれんけどさ。速度にこだわってる感があるのはいいな。圧倒的に軽いソフトを作れる人って、大体は信用出来る気がするのだ。逆も然り。
 βテスト中に使えば、βユーザー専用のライセンスが入手出来て、何かしら優遇する予定みたいなんで、この手のソフトを探してる人は今のうちに試してみるのもいいかも。

 で、約99万個のファイルが約18時間でコピー完了。おお。
 …あれ?
 NetBak Replicatorだと約92万個だったのに。まあいいか。←いいのか。
 Dropboxのキャッシュのところでエラーが発生してたけど、どーでもいいや。つーかDropboxのキャッシュ、こんなでかいファイル残ってるんかよー消せ消せー。
 ちなみにNVIDIAのドライバも巨大なゴミを残します。大抵はC:\NVIDIAC:\Program Files\NVIDIA Corporation\Installer2に。中身は全部捨てていいらしい。

 これでいいと思って運用してたら。
 メモリ使いすぎなんでこいつ落とすわ、ってWindowsのメッセージがいつの間にか出てた。マジか。おいおい。物理8GB搭載で、特に他のソフトも起動してなかったんですが。使えねー。
 つーことで、もう駄目だ話にならん。さらばBvckup。つーか、どうするんだこれー。

 結局、BunBackupの高速ファイルチェックと自動バックアップを併用して終わり。うむ。一時間ごとのバックアップになるけど、別にそれで十分だろー。メモリもまだ結構食うけど、動くからいいや。
 何かいつもBunBackupに帰ってくる感じだな…。
 ただ、空フォルダを一切処理してくれないんだよな、高速ファイルチェックだと。NAS側で処理すっかなー。うーむ。

 WindowsからNASへのミラーリングは、とりあえずこの辺にしといて。
 NAS自体のミラーリングもしないと。実はまだその為のHDDを買ってませんが。

 まあ、三重化するのは「壊れた時の復旧作業も安全にしたい」ということなので、急がなくてもいいかなーと思ってたけど、壊れた時に18時間以上無防備になるからなあ。やっぱ早めに三重化しときたい。
 でも、世代管理がハードリンク祭りなので、下手なミラーリングだと死にそう。lsyncdとrsync(-H付き)なら行けそうだけど、それよりNASのeSATAに外付けHDDを繋いで、QNAPのQ-RAID1とかいう怪しげな機能でどうにかならないかな。この方法なら、失敗しても全く損はしないのだ。今回の総入れ替えで使わなくなる外付けHDDケースがあるので。
 FSより下の層で処理してくれるようなら、ハードリンク関係の心配も無いけど、どうなんだろ。どこまで冗長化出来るのか、とにかく仕様が分からんのでまともに検討も出来ないけど、当面安上がりではあるし、2.5重化くらいの気持ちで突撃してみようか。
 怪しげな機能を試すのもネタになる、とか思ってる面もありますが。
 あと、Q-RAID1が駄目でも、eSATA接続のHDDはきっとマウントされてるだろうから、ローカルrsyncで済むかもしれない。そうするとlsyncdも使いたくなって、GCCも必要だから導入めんどくせー、って感じになりそうだけどさ。まあ、面倒なだけならな。安心して動くなら全然構わんさ。リストアがHDD突っ込むだけで済まない点も面倒だけど。

 もっとこう、ストレージの故障とかがあんまり怖くない環境を、誰でも手軽に使えるようにしてくんねーかなー。そんなのOS屋さんが主導してやってくれよー。robocopyじゃなくて、もっと誰でも手軽にさ。

追記:
 何かBvckupの作者の人からTwitterにコメントが。


ログがでかすぎたせいなのですぐ直すぜー、って感じか。
 でも、また入れ直すとまた同期し直しだよなー。BunBackupの高速ファイルチェックは空フォルダ処理してくれないから困ってないことは無いんだけど。NAS上でfind foo -type d -empty -deleteすんのもなー。んー。
 まーせっかくだから、いずれ試してみるか。

追記:
 Bvckup2の初回の同期は、同期先に既にコピーがある場合はうまいこと活用するように動作するらしい。試してないけど。