wxPythonで自由工作:作品No.001

久々のPythonネタ!
と思ったら、GAEネタも書いてましたねそう言えば。
まあ、あれはカテゴリ別なんで無視です。
と言う訳で、久方ぶりのwxPythonです。

本日取り扱うのは、みんな大好きwx.media.MediaCtrlです。
要するに、メディアプレイヤーですね。
まあ、GUIを使って何か作ろうとしたとき、大抵は頭の隅をよぎる代物ではないかと思います。
世にどれだけのメディアプレイヤーが溢れているかなんて見向きもせずに。
いいんですけどね。
実際、作りたいというのが本音であって、実用的なものに昇華しようというのは二の次です。


さて。
そんな訳で、そういうことなんですが、その前に。
どんなものでもそうですが、マルチメディアを扱うというのは、準備段階から結構手間が掛かるものです。それが実利を求めるものなら、尚のこと。僕のように遊び半分で手を出す輩程度でも、それなりに面倒なことがあります。
では。
その面倒で手間のかかる準備から、僕の失敗談も交えつつ、話を始めましょう。


環境は、Ubuntu 10.10(amd64) minimal CDでのインストールですよ。

wxPythonのwx.media.MediaCtrlを使う準備

世の中には実に様々な方がいらっしゃいましてですね、サンプルコードなんて探そうと思えば山のようにあるものなんです。
実際、僕の場合も苦もなくサンプルコードは見つけられました。
見つけること『は』できました。
ということは、そこで何かしらの問題があったということです。
第一の問題、それが旧バージョンにおける機能未実装です。


サンプルコードを実行したら、

Not Implement Error

とか出て止まる……。
何でプログラムすぐ止まってしまうん?


原因は、wxPythonのバージョンにありました。
Pythonインタプリタを起動して、

>>> import wx
>>> wx.VERSION

としてみてください。(2, 8, 11, 0, '')とか出たら見事にアウトです!
やったね!キラッ☆
てな訳で、バージョンをアップしなければなりません。


ところで、Ubuntu10.10、すなわちmaverickではどのバージョンが使えるのか?
詳しくは、下記を参考にしてください。
wxPythonのWiki
基本、ここに書いてあること実行すればいいのです。
一例を。

% sudo apt-get install curl (インストールしてないのなら)
% sudo apt-get remove --purge python-wxgtk2.8 python-wxtools wx2.8-i18n
(古いものが入っていたら消す)
% curl -x http://proxy.hogehoge:8080 http://apt.wxwidgets.org/key.asc | sudo apt-key add -
(curlでプロキシを使うには、-xオプションで)
% sudo vi /etc/apt/sources.list(ページ参考に編集を)
% sudo apt-get update
% sudo apt-get install python-wxgtk2.8 python-wxtools wx2.8-i18n

これで、バージョンが(2, 8, 12, 0, '')ならオッケーです。


さて。
これでめでたくwx.media.MediaCtrl()が使えるようになりましたかね?
おやおや?今度は別のエラーが出ているぞ?
このエラー量産機が……。
ああ、一応言っておきますよ。
僕のことではありません。

Got an invalid playbin

で。
これは、バックエンドのライブラリ不足が問題でした。


MediaCtrlは、WindowsならDirectShow、MacならQuickTimeLinuxならGStreamerをバックエンドで利用しているそうです。
つまり、wxPython単体の環境を揃えても、それらが整っていないとMediaCtrlが機能しないということです。


なので。
Ubuntuな僕は以下をインストールしときました。

% sudo apt-get install libgstreamer{-plugins-base0.10-0,0.10-0} gstreamer0.10-{alsa,fluendo-mp3,ffmpeg,nice,plugins-{base,bad,good},pulseaudio,tools,x}

これで、mp3やmp4ぐらいは再生できるようになります。
少なくとも、mp3の再生は確認済です。
ただし。
Linuxにおいては、(もしかしたらMacでも)wmaやwmvはこのままでは出来ません。
出きるようにしていないので何とも言えませんが、おそらくそれなりの設定やライブラリが必要になります。
なんたって、Windows Media AudioにWindows Media Videoだもの。


そんなこんなで。
大体、躓いたのはこんなところですね。
僕の場合は、これでサンプルコード動きました。

サンプルコード

と言う訳で。
僕がちょっと手を加えた、けれどもほとんど参考にしたまんまのサンプルコードが以下です。

#!/usr/bin/python
import wx
import wx.media
import os

class MediaPlayerPanel(wx.Panel):
    def __init__(self, parent,id):
        wx.Panel.__init__(self,parent,-1,style=wx.TAB_TRAVERSAL|wx.CLIP_CHILDREN)
        self.SetBackgroundColour("#BCFFCD")

        try:
            self.mc = wx.media.MediaCtrl(self, style=wx.SUNKEN_BORDER)
        except NotImplementedError:
            self.Destroy()
            raise # program exit.

        loadButton = wx.Button(self, wx.ID_ANY, "Load File")
        self.Bind(wx.EVT_BUTTON, self.onLoadFile, loadButton)

        playButton = wx.Button(self, wx.ID_ANY, "Play")
        playButton.SetToolTip(wx.ToolTip("load a file first")) # message when on mouse
        self.Bind(wx.EVT_BUTTON, self.onPlay, playButton)

        pauseButton = wx.Button(self, wx.ID_ANY, "Pause")
        pauseButton.SetToolTip(wx.ToolTip("press Play to resume"))
        self.Bind(wx.EVT_BUTTON, self.onPause, pauseButton)

        stopButton = wx.Button(self, wx.ID_ANY, "Stop")
        stopButton.SetToolTip(wx.ToolTip("also resets to start"))
        self.Bind(wx.EVT_BUTTON, self.onStop, stopButton)

        self.slider = wx.Slider(self, wx.ID_ANY, 1000000, 0, 1000000,
            size=(410, -1),
            style=wx.SL_HORIZONTAL|wx.SL_AUTOTICKS|wx.SL_LABELS)
        self.slider.Bind(wx.EVT_SLIDER, self.onSeek)

        ext = "load .mp3 .mpg .mid .wav .wma .au or .avi files"
        self.st_info = wx.StaticText(self, wx.ID_ANY, ext, size=(300,-1))

        sizer = wx.GridBagSizer(vgap=5, hgap=5)
        sizer.Add(loadButton, pos=(1,1))
        sizer.Add(self.st_info, pos=(1,2), span=(1,2))
        sizer.Add(self.slider, pos=(2,1), span=(2,3))
        sizer.Add(playButton, pos=(4,1))
        sizer.Add(pauseButton, pos=(4,2))
        sizer.Add(stopButton, pos=(4,3))
        # for .avi or .mpg video files use lower grid area
        sizer.Add(self.mc, pos=(5,1), span=(7,3))
        self.SetSizer(sizer)

        self.timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self.onTimer)
        # update every 100 milliseconds
        self.timer.Start(100)

    def onLoadFile(self, evt):
        mask = "Media Files|*.mp3;*.mpg;*.mid;*.wav;*.au;*.avi|All (.*)|*.*"
        dlg = wx.FileDialog(self,
            message="Choose a media file (.mp3 .mpg .mid .wav .wma .au .avi)",
            defaultDir=os.getcwd(), defaultFile="",
            wildcard=mask, style=wx.OPEN|wx.CHANGE_DIR)
        if dlg.ShowModal() == wx.ID_OK:
            path = dlg.GetPath()
            self.doLoadFile(path)
        dlg.Destroy()

    def doLoadFile(self, path):
        if not self.mc.Load(path):
            wx.MessageBox("Unable to load %s: Unsupported format?" % path,
            "ERROR", wx.ICON_ERROR|wx.OK)
        else:
            folder, self.filename = os.path.split(path)
            self.st_info.SetLabel("  %s" % self.filename)
            # set the slider range min to max
            self.slider.SetRange(0, self.mc.Length())
            self.mc.Play()

    def onPlay(self, evt):
        self.slider.SetRange(0, self.mc.Length())
        s1 = "  %s" % self.filename
        s2 = "  size: %s ms" % self.mc.Length()
        self.st_info.SetLabel(s1+s2)
        self.mc.Play()

    def onPause(self, evt):
        self.mc.Pause()

    def onStop(self, evt):
        self.mc.Stop()

    def onSeek(self, evt):
        """allows dragging the slider pointer to this position"""
        offset = self.slider.GetValue()
        self.mc.Seek(offset)

    def onTimer(self, evt):
        """moves the slider pointer"""
        offset = self.mc.Tell()
        self.slider.SetValue(offset)

def main():
    app = wx.PySimpleApp()
    frame = wx.Frame(None, -1, "play audio and video files", size = (500, 500))
    MediaPlayerPanel(frame, -1)
    frame.Show(True)
    app.MainLoop()

if __name__ == "__main__":
    main()

参考URL


まあ、細かい説明は省きましょう。
プログラム載せたものだから、相当長い上に重い記事になってしまいましたし。
GitHubでも使えばいいんですかね?同じことかな?
ともかく。
シンプルなプレイヤーの一例です。この雛形さえあれば、後はカスタマイズし放題ですね。
イヤッフゥ!

あとがき

ちなみに。
なんで、上記のサンプルコードがFrameではなくPanelなのかというと、その方が汎用性が高いと思うからです。
Notebookとかにも使えるし。
なので、僕は基本的にはPanel上でアプリケーションの作り込みをしています。
で、必要に応じて他プログラムでimportして使う。
そういう背景から、if __name__〜の記述があるという訳なのです。


こんな感じで。
今日はお暇させていただきます。

KVMで暇を潰そうの会:会員No. 001の報告

最初に言っておきますが、そんな会は実在しません。
非実在同好会です。
仮に存在したとしたら、関係者の方には申し訳ないですけど。
最初は『〜で遊ぼうの会』にしようと思ったんですが、ググったら割とヒットしたのでやめました。
まあ、そんな訳で。
会員は募集しておりませんので、ご了承ください。


さてさて。
まあ、このブログがどんなものなのか、もっと言えばどういう記事が書かれているかを把握されておられる希少な方がいらっしゃったとすれば、こう思うことでしょう。
「こいつ、やることがころころ変わるなあ……」
呆れ半分、諦め半分で。
反論はありません。
ああ、『ありません』というのは、まさに言葉通りの意味であって、『反論はあるけど、ここではしない』ではなく、『ないものはないんです。本当に、困ったものです』ということです。
「そんなことじゃ何も身につかないぜ?」とでも言いたげな目はやめてください。
分かってますから。
僕は何でも知ってるんです。


えっへん。


てな訳で。
今回は、KVMです。
Kernel-based Virtual Machine、略してKVMですね。
決して、『彼女は仮想機械』の略ではありません。
ある意味、間違ってないと思いますけどね。
ラノベのタイトルとかにありそうじゃないですか?
ただまあ、もしあったとしたら、そんな彼女が規制されないことを祈るばかりです。


話が飛んでばかりですね。
主題とは全く別ベクトルに話題が富んでいるとも言いますが。
ときに、一通さんは話題のベクトルも自由自在なんでしょうか。
ああ、大丈夫ですね。
考えてみれば、考えるまでもなく彼はコミュニケーションにおいてもほぼ一方通行でした。
それでも、最近はそうでもない印象を受けますけどね。
アクセラレーターかわいいです。
彼が怪異だとしたら、間違いなくダイレクトに影響を与えているのは打ち止めでしょうけどね。
………………
どっちも常識が通用しねぇ……。


閑話休題
そろそろ、話を始めましょう。
話の構成としては、ざっくりと、

  1. インストール?
  2. 使い方?
  3. KVMって何?

みたいにしようと思ってます。
とりわけ、使い方?の部分では、Arch LinuxWindows7FreeBSDをそれぞれ試験的にインストールした際の話をしようかな、と、そんな風に思ってます。
始めてこのブログに目を通される方で、この記事に目を通す価値があるか判断しかねている方は、『はじめに』カテゴリを参照していただければ、大方の目安になるかと思います。
参考までに私見を示しておくと、ほとんどないと思います。
自分で書いておきながら、とお思いでしょうけれど、でも自分で書いたから分かることもあるでしょう?
そういうことです。
……どういうことだ?

KVMのインストールについて

僕はUbuntuユーザーなので、ここでの話はUbuntuにおけるものと解釈してください。
もっとも、貴方がUbuntuユーザーなら、ここでの話はとても簡単です。


まず、第一にハードウェア的に環境が満たされているかをチェックしましょう。
最も肝心なのは、CPUが仮想化支援機能を有しているか否かです。
IntelのCPUをお使いなら、

% grep vmx /proc/cpuinfo

AMDのCPUをお使いなら、

% grep svm /proc/cpuinfo

のコマンドで、それぞれのサポート状況をご確認ください。
結果、たとえばIntel系の場合なら、

% grep vmx /proc/cpuinfo
flags           :(前略) vmx(後略)

のようになっていれば、OKです。AMD系ならvmxをsvmに置き換えてください。
これが、最初の関門です。
残念ながらフラグが立っていなかった方は、KVM使えません。
いや、手元にサポートしていないCPUを積んだマシンがないので検証できてないんですけど、まあできません。
お気持ちは察しますが、僕にあたらないで下さい。
でも、最近のものなら割とサポートしてると思います。
まあ、マシン買い換えるってのも、お金、かかっちゃいますからね。
とにかく、この基準を満たしているかどうかの確認を最初に行って下さい。


で。
このハードな関門を乗り越えれば、インストール自体は楽ちんです。
拍子抜けです。
以下のコマンドで、必要なパッケージのインストール終了です。

sudo apt-get install qemu-kvm kvm-pxe

ね?
簡単でしょう?
ただ、ちょっと準備が要ります。
お使いのCPUがIntel系なら、

% sudo modprobe kvm
% sudo modprobe kvm_intel

AMD系なら、

% sudo modprobe kvm
% sudo modprobe kvm_amd

のそれぞれのコマンドを実行して、モジュールをロードしてください。
KVMは、現在ローダブル・カーネル・モジュールとして実装されていて、モジュールがローダブルと言うよりモジュールをロードしないと使えません。
あとあと、起動時に自動で読み込んでくれるように、/etc/modulesに以下を追記しとくといいと思います。

kvm
kvm_intel(あるいはkvm_amd)

最後に、モジュールがロードされているかの確認もしてみます?

% lsmod |grep kvm

あとは、まあ、kvmグループにでも登録しておきましょう。

% grep kvm /etc/group
とすると、kvmグループが作られていることでしょう。
任意のユーザーを所属させるには、
% sudo gpasswd -a <user-name> kvm

これで、大方の準備は終わりです。
お次は、使い方?行ってみましょう。

追記

CPUが仮想化をサポートしているにも関わらず、kvm_intelをロードしようとすると、

FATAL: Error inserting kvm_intel (/lib/modules/2.6.35-28-generic/kernel/arch/x86/kvm/kvm-intel.ko): Operation not supported

みたいなエラーメッセージと共に失敗する場合は、BIOSの設定を怪しんでください
具体的な一例(あくまで例ですので、ご自身の環境に置き換えて見てください)として、BIOSのConfigとかからCPUの設定に入り、Intel(R) Virtualization TechnologyやVT-d FeatureがEnableになっているかどうか、それを確認してください。
これが、Disableになっていると何してもロードできません

KVMを使う

インストールできたら、後は使うだけですね。
まあ、本来こちらがメインですけれど。


KVM仮想マシンにインストールを行う際の手順は、

  1. 仮想ディスクを作る
  2. インストールする

という豪華2本立てになっています。
これらは、それぞれ該当するコマンドで行います。


仮想ディスク、って言ったらいいんですかね。
要するに、現在のファイルシステム上にインストールする仮想OSのための領域確保です。
仮想的なハードディスクドライブを作ると言えば、より簡易でしょうか。
そのコマンド例が以下です。

% qemu-img create -f qcow2 virtual-hdd.img 20G

細かい説明は、細かい説明をしてくれるところに回すとして。
qcow2とは作成する仮想HDDのフォーマットのことなんですが、qcowおよびqcow2は実際に使った分だけの容量を消費します。
つまり、この例では20ギガバイトのHDDを仮想的に作っていますが、いきなり20Gが占領されるわけではありません。
大抵のKVMの紹介では、qcowやqcow2を使っています。
ただし。
僕は、rawフォーマットでの作成を推奨します。
rawは固定フォーマットなので、20G予約したら20Gを占領します。
ただ、qcow*(qcowとqcow2の意)フォーマットよりも早いです。
なぜか。
qcow*は使った分だけを消費しますから、それは逆に言うと、データの書き込みが起こる度に、ホストOSのファイルシステム上にゲストOSのファイルシステムをエミュレートすることに他なりません。
つまり、データの書き込みの度に、ディスクをフォーマットしていることになります。
経験から結論を言うと、qcow*遅いです。
環境によって変わるのかもしれないですが、遅いです。
後述しますが、qcow2フォーマットのメモリ512MB予約でUbuntu minimalインストールした時なんて、半ギレしました。
遅すぎます。
今時、個人のHDDリソースなんて、消費しきれるものではないでしょう?
重要なデータは、基本外付けHDDでバックアップをとっているでしょうし。
ストレスフリーとまではいきませんが、可能な限り快適な環境を求めるなら、

% qemu-img create -f raw virtual-hdd.img 20G

がいいと思います。
imgファイルの名称とディスクサイズはご自由に。
僕は、試験的に導入したwindows7には100Gをあてがいました。


で。
容量予約を済ませたら、次はいよいよインストールですね。
KVMはCD/DVDドライブに挿入したインストールディスクからのインストールと、isoイメージからのインストール、どちらにも対応しています。
まあ、windows系は別としても、isoからのインストールの方がディスク焼かなくていいんで楽です。
isoイメージファイルからのインストール例は、

% sudo kvm -no-acpi -m 1024 -cdrom ./mini.iso -hda virtual-hdd.img -boot d

各オプションについて
-no-acpi: acpiサポートを無効にします。後述しますが、エラーの原因になりやすいです。
-m 1024: メモリの予約です。ここでは、1024MB、つまり1ギガを予約しています。
-cdrom: インストールメディアの指定です。ここでは、isoイメージファイルを指定しています。
-hda: インストールを行う仮想HDDです。qemu-imgで作った任意のものを指定してください。
-boot: ブート先。インストール作業を行うときはdを指定してください。

ですね。
特筆すべきことは、メモリの予約についてでしょうか。
ここで予約できるメモリの上限は、おそらく実メモリの半分以下です。
具体例をあげると、貴方のマシンが4Gのメモリ搭載なら、2Gまでは予約可能ですが、2.1Gはきっと無理です。
ドキュメントをまだよく読んでないのですが、この制約を破ると『〜Gのメモリのエミュレートは無理だって!』とエラー吐かれます。
まあ、Linuxぐらいなら、1Gあれば事足りるでしょう。
貧弱な環境を長らく使っていた僕なんかに言わせたら、おもちゃに1Gもメモリを割り当てられるなんて素敵です。


仮に、貴方がCD/DVDからインストールするならば、たとえば、

% sudo mount /dev/sr0 /media
% sudo kvm -no-acpi -m 1024 -cdrom /media -hda virtual-hdd.img -boot d
(作業終了後)
% sudo umount /media

みたいな感じでしょうか。
僕、minimalでいつもUbuntuインストールするので、しかもどのマシンもCD/DVDドライブ搭載してないので、いつも外付けなんですよ。
なんで、例における/mediaの部分は、ドライブが認識されていれば、それに置き換えてください。
自動でドライブがマウントされるなら、mountのくだりも不要です。
今回は書き込みではなく読み込みなので、自動マウントされないのなら、おそらくマウント必須です。
後は、普通にインストールするだけですね。
ネットワークはDHCPで構わないと思います。ただ、プロキシにはご注意を。
……プロキシめ。
ntpdateが使えねぇとか……仕方ないけどさ。

ちなみに。
うっかり仮想OSのウィンドウをクリックしてしまって抜け出せねぇ!!となった場合
ウィンドウトップのメッセージに従ってください。
すなわち、『Ctrl-Alt』を押してください。
解放されます。


インストールが終わったら、後は普段の使い方ですね。

% kvm -no-acpi -m 1024 -hda virtual-img.img -boot c&

bootオプションにcが渡されていることに注意してください。
通常起動時は、dではなくcになります
このコマンドを実行すると、QEMUというエミュレータが起動し、インストールしたOSが立ち上がったらハッピーです。


さてさて。
では、僕がそれぞれ試しにインストールしてみたときのことを、参考までに記しておきましょう。
まずは、Ubuntu minimal。

% qemu-img create -f qcow2 vm-disk.img 20G
% sudo kvm -no-acpi -m 512 -cdrom ./mini.iso -hda vm-disk.img -boot d

これ、インストール完了までに6時間以上かかりました。
core-i7だぜ?メモリ、4ギガだぜ?
実マシンでやったら、6時間で環境の構築までが大方終わるってのによォ。
………………
僕は、この一件で懲りに懲りて、メモリは1ギガ、形式はrawを使うようになりました。
ただ、メモリに関してはちょっと疑問です。
ゲストOSでtopしても、そんなに無いような……。
この辺は勉強不足です、すみません。


次がArch Linux
何か、僕のまわりで人気沸騰中なんですよ。
なぜ?

% qemu-img create -f raw arch.img 20G
% sudo kvm -no-acpi -m 1024 -cdrom ./arch.iso -hda arch.img -boot d

こちらは、とりたてて問題なく。
せいぜい、キーマップとプロキシという、個人的な問題だけでした。


ここで、Xの環境構築についてですが、どうもデフォルトのままではダメなようです。
Openbox使ってるんですが、X起動しても落ちる方は、Xorgの設定でScreenセクションのDefaultDepthを16にしてください。
必要なドライバ入ってるはずなのに、って場合は、大概それでOKです。
/etc/X11/Xorg.confの例としては、

Section "Screen"
    Identifier     "Screen1"
    Device         "Device1"
    Monitor        "Monitor1"
    DefaultDepth    16
    Option         〜
    Option         〜
    SubSection     〜
        Depth       16
    EndSubSection
EndSection

Archなら/etc/X11/xorg.conf.d/10-monitor.confをいじればいいんですが、まあArchWikiのXorgに詳しいでしょうから、そちらに譲ります。


で。
問題なのが、FreeBSDWindows7ですね。
結論から言うと、-no-acpiオプションは外してください
このオプションつけたままでは、インストール進みません。
起動の段階で止まります。
FreeBSDの場合。

% qemu-img create -f raw freebsd.img 20G
% sudo kvm -m 1024 -cdrom ./freebsd.iso -hda arch.img -boot d

Windows7の場合。これは、CD/DVDドライブ使いました。

% qemu-img create -f raw win7.img 100G
% sudo mount /dev/sr0 /media
% sudo kvm -m 1024 -cdrom /media -hda win7.img -boot d
% sudo umount /media

まあ、後はあげつらうべき問題はありませんでした。
ちなみに、FreeBSDxorgのconfigure中に『VMware-video』の文字を見かけてチェック入れましたが、やっぱりDefaultDepthは16が無難です。
素敵です。
xorgのmake installさえ、2時間あれば終わりました。
かつての非力なマイマシンは、2倍以上はかかったというのに……。
実は3倍以上だけどね!キラッ☆……環境構築に連日徹夜したのは、いい思い出

まあ、基本的にはこんなもんですかねぇ。
もう少し、ネットワークの設定とか、色々便利には出来るんですけど、それはまたにしましょう。
この記事、とんでもなく長くなっているので。

KVMってそもそも何すか?

冒頭でも……言ったっけ?まあ、Kernel-based Virtual MachineKVMです。
Wikiとかに有益な情報がまとまってるんですが、KVMWikiと併せて、少なくともハイパーバイザWikiには目を通してください。仮想化技術に関して、とても簡潔に把握できると思います。


要は、KVMでは、ホストOS上でゲストOSが動くと言うよりも、ホストOSとゲストOSは同次元にあって、両者のリソース配分をKVMが管理しているという具合でしょうか。
同次元は言い過ぎかな。
でも、まあホストOSがゲストOSを管理しているわけではないということです。
仮想的な仮想化ではなく、より本質的なと言うか本来のイメージでの仮想化技術ですね。


まあ、この辺は僕も把握しきれていないので、これぐらいにしておきます。
ただ、手っ取り早く知った気になりたい方は、とにかくハイパーバイザのWikiを見てください。
ぶっちゃけ、記事編集に疲れました……

終わりに

僕がKVMを使うのは、技術的な好奇心と言うより、色んなディストリビューションやOSを触ってみたいと思ったからです。
特に、FreeBSDなんかは練習がてらに使うのにもってこいですし。
プログラムの環境依存の調査とかにも便利そうですし。
単純に、いくつもOSが動いてるってすごいことですし。


まあ。
いくつもOS立ち上げたところで、僕の処理能力は変わりませんが。
ではでは、今日はこの辺で。
長文、失礼しました。長文、めっさ疲れた……

GAE Tips Vol.3

春寒ようやくぬるみ始めたこのごろ、皆さんいかがお過ごしでしょうか。
僕は、元気……うーん、まあ、元……気ではないですけど。

って言うか。

4月も終わるというのに寒すぎです。
夜とか。
日落ちると、けっこう肌寒いです。
今年は、春の訪れがあまり感じられません。そりゃ、桜とか咲いてたりしますから、春なんだなと思うことはありましたが、実感として体感として、それを感じることが少ないです。
まあ、長らく春の訪れたことのない僕の人生に比べればマシですが。
………………。
前文の解釈はご自由にどうぞ!


さて、お題はGoogle App Engineですね。
今回も適当に書き散らかします。


その前に、一つアナウンスを。
Nora's Ark
性懲りもなく、という感じですけれど。
一応、例の何にもないサイトよりは、いくらかマシなブログになってます。
ただの番宣ですみまませんが、一応この場を借りて紹介させていただきます。
見るか見ないかは、貴方しだいです。
見るか見ないか(の責任)は、貴方(の良心)しだいです。
※空白が気になるみんなは、ドラッグしてみよう!


いいとして。
今回は、宣伝したブログを作るに当って気になったことのまとめです。

1つのスクリプトファイルで複数のページを制御

まず、以下はあるapp.yamlの一部抜粋です。

-  url: /option
   script: script/option.py

-  url: /option/.*
   script: script/option.py

たとえば、ページの入力フォームで入力したテキストをデータストアに保存する、ちょうどGAEのチュートリアルにあるような機能をイメージしてください。

チュートリアルにおけるapp.yamlは、

- url: /.*
  script: helloworld.py

みたいになっていたかと思います。

なぜ、チュートリアルのように一まとめでなく、2段階に分けてあるのか。
たとえば、option.pyが、

<前略>

class OptionPage(webapp.RequestHandler):
   def get(self):
      ......
      path = os.path.join(os.path.dirname(__file__), 'option.html')
      self.response.out.write(template.render(path, template_values))

class OptionPagePost(webapp.RequestHandler):
   def post(self):
      ......
      self.redirect('/option')

class OptionDevPage(webapp.RequestHandler):
   def get(self):
      ......
      path = os.path.join(os.path.dirname(__file__), 'option-dev.html')
      self.response.out.write(template.render(path, template_values))

class OptionDevPagePost(webapp.RequestHandler):
   def post(self):
      ......
      self.redirect('/option-dev')

(中略)

applicationn = webapp.WSGIApplication(
                                     [('/option', OptionPage),
                                      ('/option/sign',OptionPagePost),
                                      ('/option-dev',OptionDevPage),
                                      ('/option-dev/sign',OptionDevPagePost)],
                                     debug=True)
(後略)

という構造になっているとしましょう。
ファイル構成は、

project/
  |-----option.py
  |-----option.html
  |-----option-dev.html

という具合です。

ちなみに、ここで出てくる〜/signというアドレスは、テンプレートとなるHTMLファイルにおける、

<form action="/option/sign" method="post">
  <div><textarea name="content" rows="3" cols="60"></textarea></div>
  <div><input type="submit" value="Add"></div>
</form>

の、formのaction部分に依存しています。ここを変えれば、signでなくとも構いませんが、スクリプトファイルとの整合性には注意が必要です。
※整合性がとれないと、アドレスが噛み合わずアクセスできなくなります。

本題に戻りましょう。

ここで、

-  url: /option
   script: script/option.py

-  url: /option/.*
   script: script/option.py

-  url: /option-dev/.*
   script: script/option.py

とすると、option-devというページが見つからないというエラーになります。


さて。
実を言うと、解決法が分かっただけで、原因はよく分かっていません。
て言うか、何一つ分かっていません。えっへん!
……いえ、嘘です。
エラーには、指定されたアドレスにマッチするURLが存在しません、みたいなことを言われました。
なので、おとなしく、

-  url: /option
   script: script/option.py

-  url: /option/.*
   script: script/option.py

-  url: /option-dev
   script: script/option.py

-  url: /option-dev/.*
   script: script/option.py

としたら、Not Foundエラーから解放されました。


予想としては、以前にもご紹介したスクリプトファイルと静的ファイルの格納場所の違いとか、ファイルの配置の問題、app.yamlの書き方ミスなどが考えられますが、僕自体が何一つ確かなものを持たないので、何とも言えません。
あるいは、全部僕が悪いのかもしれません。
ともあれ、1つのスクリプトファイルで複数のページを制御する際は、app.yamlが重要になってくることは念頭に置いておく必要がありそうです。
どこに原因があるかの見当がつくだけでも、バグの対処が少しは楽になりますからね。

自分のウェブページでDatastoreのデータ削除

実装経験があるのは、一括ではなく一個ずつです。
まあ、応用すれば一括指定ぐらいは楽にできると思います。
文章が走っているのは、眼が痛いからです。ドライアイが……。

前置きは置いといて。
今回ご紹介する方法では、数値IDを利用します。
とりあえず、参考にした公式ドキュメントです。
Google App Engine Ref. for Model


数値IDっていうのは、たとえば、チュートリアルのテンプレートを使った場合の例で出てくるHTMLファイルで、

    {% for greeting in greetings %}
      {% if greeting.author %}
        <b>{{ greeting.author.nickname }}</b> wrote:
      {% else %}
       An anonymous person wrote:
      {% endif %}
      <blockquote>{{ greeting.content|escape }}</blockquote>
    {% endfor %}

って部分がありますよね?このとき、

greeting.key.id

で得られるのが、数値IDです。
各データには、長ったらしい一意なキー値が割り当てられますが、数値IDは文字通り数字だけで表された、簡単なキー値です。


さて、上記のチュートリアルのコードをちょっと変えて、

    {% for greeting in greetings %}
      {% if greeting.author %}
        <b>{{ greeting.author.nickname }}</b> wrote:
      {% else %}
       An anonymous person wrote:
      {% endif %}
      {{ greeting.key.id }}
      <blockquote>{{ greeting.content|escape }}</blockquote>
    {% endfor %}

なんてすると、各書き込みの数値IDが表示されるようになります。
つまり、この数値を指定することで、該当する書き込みをDatastoreから削除できるというわけです。
そのステップを簡単に表すと、

  1. 数値IDを入力
  2. スクリプトで受け付ける
  3. 削除

です。


まず、受付と削除から。
これは新たに削除専用のクラスを作りました。

class Greetings(db.Model):
    ........

class OptionDel(webapp.RequestHandler):
    def post(self):
        greetings = Greetings()

        keys = self.request.get('keys')

        try:
            keys_int = int(keys)
            # Null message don't strage.
            if greetings.get_by_id(keys_int) != None:
                greetings.get_by_id(keys_int).delete()
        except ValueError:
            pass

        self.redirect('/option')

......

applicationn = webapp.WSGIApplication(
                                     [......
                                      ('/option/del',OptionDel)],
                                     debug=True)

もちろん、app.yamlは先述の通り、適切に書き記して置きます。(signがdelに変わるだけですが)
で。
try構文使ってるのは、数値以外の書き込みを弾くためです。
int()はstringをintに変換する関数ですが、数値になるような文字列以外を渡すとエラー吐いて、止まりやがります。なので、余計なものを無視する機構を組んだというわけです。
肝は、get_by_id()ですね。これで、数値IDで任意のデータを指定できます。


入力の方は、書き込みの応用ですね。

<form action="/option/del" method="post">
  <div><textarea name="keys" rows="1" cols="10"></textarea></div>
  <div><input type="submit" value="Delete"></div>
</form>

対応するHTMLファイルに、これを追記するだけです。


とまあ、以上です。
あっさりし過ぎですか?
僕なんかあんまりにもあんまりな頭の出来具合なので、この程度でも結構濃厚でしたけど。
でも、これで割とデータの管理がしやすくなりました。
宣伝したブログで実際に使ってますけど、書き込むの僕だけなので削除も気兼ねなくできますし。
応用が効くと言ったのは、つまり入力と受付を工夫する─たとえば、リストで受け付けるとかってすれば、一括指定も可能という意味です。
それは割とすぐ出来そうですね。

終わりに

することがないとだらけるし。
することがあると余計なことをする。
僕は自分がそんなどうしようもない人間らしき存在であることを、改めて認識しました。
いや、しなきゃいけないこと、結構あるんですよねー。(棒読み)
でもでも、そういう時ほど、別のことが捗っちゃったりしません?
掃除とかね。
それから、掃除とか。
あと……掃除とか。
………………。
忙しさにかまけて怠惰な私生活を送っていたつけが大きすぎました。
そんな訳で。


相変わらず、忙しい毎日ですが、それなりに元気にやってます。
まあ、空元気とも言いますが。
ではでは。
こんなヘタレブログの腐れ記事に目を通していただいて、ありがとうございました。
本日はこの辺で失礼します。

echoのまとめ

個人的に、使う機会の多いechoコマンド。
実は存外オプションがたくさんあるのです。
知ってどうするの?は禁句です。

今回はまあ、さくっと行きます。

オプション

-n:行末の改行を行わない。
-e:文章中のバックスラッシュでエスケープされた文字列の解釈を有効にする。
  ※ 使用可能な機能一覧
   \a:アラート
   \b:バックスペース
   \c:行末の改行を出力しない
   \f:フォームフィード(form feed)
   \n:改行(new line)
   \r:復帰 (carriage return) 
   \t:水平タブ
   \v:垂直タブ
   \\:バックスラッシュ(そのまま出力)
   \0nnn:アスキーコードが nnn (8 進) の文字

--help:ヘルプ表示
--version: バージョン情報表示

文字装飾(書体変更、色付け)

echo -e "\033[装飾;文字色;背景色mテキスト(\033[0;39m)"

※()部分はいわゆる、フォームリセット。まあ、無くてもいいけど、普段はつけよう。

いきなりでしたが。
上記が基本的な書式となります。
で、以下がそれぞれの書式となります。

装飾

0:装飾なし
1:太字
4:下線
5:点滅
7:色反転
8:Concealed on

ただし、手前の環境で効果を確認したのは、0、1、4、7、8だけです。
また、太字にすると色が若干変わりました。この辺は、使っているシェルの設定に起因するのかもしれません。
ちなみに、分かり辛いだろう、そして日本語訳がわからない8の『Concealed on』は、背景色で塗りつぶす効果を与えます。

文字色

30:黒
31:赤
32:緑
33:黄
34:青
35:マゼンタ
36:シアン
37:白

こちらも(そして背景色も)、使っているシェルの色設定に影響を受けるかもしれません。
手前のディスプレイの発色が悪いだけもしれませんが。

背景色

40:黒
41:赤
42:緑
43:黄
44:青
45:マゼンタ
46:シアン
47:白

ま、文字色の十の桁が3から4になっただけですね。

用例

echo -e "\033[1;34mtext\033[0;39m"
(太字、文字が青色)

echo -e "\033[1mtext\033[0;39m"
(太字)

echo -e "\033[44mtext\033[0;39m"
(背景が青)

echo -e "\033[1;44mtext\033[0;39m"
(太字、背景が青)

echo -e "\033[31;1;44mtext\033[0;39m"
(文字色が赤、太字、背景が青)

みたいな感じで。

注意

ここに書かれているものと異なる挙動を示す場合、お使いのechoが何なのかを調べて見てください。

たとえば、

which echo

というコマンドの結果が、

echo: shell built-in command

の場合、挙動が変わる可能性大です。これは、シェル特有のコマンドとしてechoが準備されている場合のものです。
この記事で扱っているechoは、Ubuntuにおける/bin/echoです。

また、お使いのシェルによって、オプションの挙動も変わります。
要するに、結果が環境に大きく左右されるということです。

まとめ

まあ、自分で書いといてなんですけれど。
何やってんでしょうねぇ、この子。

ともかく。

echoコマンドについて、ちょっとしたまとめでした。

初心者のGit Vol.1

何の初心者かは分かりませんが。
いいんです。こういうものは、語呂が命ですから。

さて。

今回は、

という内容を、簡単にメモしたいと思います。
『簡単に』なんて前置きをしなくても、簡単じゃないメモなんて僕程度には書けませんが。
ちなみに、ここはGitの説明は行わない、不親切設計となっております。

UbuntuにGit導入

まあ、aptとか使えば一発なんですけどね。
ただまあ、間違っても、

sudo apt-get install git

なんてしてはいけませんが。
正しくは、

sudo apt-get install git-core

です。前者は罠なんです、手の込んだいたずらなのです。
……いや、まあ違いますけどね。
前者で入るのは『GNU Interactive Tools』です。今は、gnuit?なのかな、まあとにかく、ここで扱いたい分散バージョン管理ツールのGitとは無縁の長物です。
紛らわしいので、改めて書き記して置きますが、分散バージョン管理システムのGitをインストールしたければ、Ubuntuでインストールするパッケージはgit-coreです。(ソースからビルドする場合は度外視)

で。

インストールしたら、まずはユーザー設定を行います。

git config --global user.name "hoge foo"
git config --global user.email "hoge@hoge.com"

名前、メールアドレスともに、コミットしたユーザー情報として登録されるものです。

ここで、configのオプションである--globalですが、これはローカル全体の共通設定として登録する際に追加するものです。(ローカル全体と言っても、特定ユーザーにとっての全体で、ローカルシステム全体ではありません)
ローカル全体の共通設定は、ホームディレクトリの.gitconfigファイルに記されています。

後は、お好みですね。僕は大概、以下の設定を加えます。

git config --global color.ui "auto"
git config --global alias.st "status"
git config --global alias.conflis "config --list"
git config --global push.default "matching"

color.uiをautoにすると、git statusなどをしたときに、変更されたものを、ステージされた変更をで表す、と言ったように、見た目にわかりやすく表示してくれるようになるので便利。

aliasはエイリアス。上の例で言えば、たとえばgit stgit statusと同じものになる。リポジトリ単位のエイリアス設定も可能。設定したエイリアスの確認方法は、

の3種類。
まあ、最後の1つがあれば十分だと思う。

push.defaultは、まあリモートでも管理するようになったときに必要になるんじゃないかと思う。以前の経験として、pushしたときにそういう操作をシステムから要求されたことがあるので、保険みたいな気持ちで設定しています。

導入は大体こんな感じです。

ローカルでリポジトリ作成

と言っても、あんまり書くことないんですけどね。
たとえば、testというレポジトリを作るとしましょう。

mkdir test
cd test
git init

これで終了です。もう、Gitが管理してくれるようになっています。
超お手軽ですね!
使いこなすのは難しいけど。

リモートレポジトリ作成

さて、個人の場合でも作業環境があちこちにあって、育てているプロジェクトのバージョン管理が煩雑というのは、よくあることと思います。
一番理想的なのは、まあ、Gitとサーバーの連携なんですけど、それが中々叶わない場合もあるでしょう。

ともあれ。

リモートレポジトリの作成です。
今回は、USBメモリに作ることにしましょう。
※ここに記しているのは、僕がいつもしている方法であり、一般的か否かはわかりません。ご注意を!

ローカルに、既にtestというレポジトリがあるとしましょう。
USBメモリは/mntにマウントされ、アクセスには管理者権限が必要と仮定します。
これらの前提に則って、リモートレポジトリを作るとしたら、

cd /mnt
sudo mkdir test.git
cd test.git
sudo git --bare init (リモートリポジトリの作成)
cd ~/test (ローカルのリポジトリ)
git remote add origin /mnt/test.git
git config branch.master.remote "origin"
git config branch.master.merge "refs/heads/master"
sudo git push origin master

としています。sudoがついているものは、USBメモリの書き込みに管理者権限が必要な場合です。不要である場合は、sudoをつける必要性は当然ながらありません。

追記(2011/04/20)

USBメモリなどへのアクセスに管理者権限な必要な場合の補足です。

始めてsudo git pushする際、.git/logs/refs/remotes/origin/masterというファイルが作成されます。
ただし、sudoつけてやると、このファイルは、

-rw-r--r-- 1 root root 560 2011-04-20 11:08 master

みたいな感じで作成され、git pullにまでsudoが必要になってしまいます。

そこで、該当ファイルの所有者を変えて使います。

% cd $(REPOSITORY_PATH)/.git/logs/refs/remotes/origin
% sudo chown <user-name>:<user-group> master

まあ、-Rオプションで一括で片付けてもいいのでしょうけれども。
以上、追記、失礼しました。


git remote addで、ここではUSBメモリ上に作っているのでこうなりますが、サーバーなら、たとえば、

git remote add origin ssh://hoge@foo.org/git/test.git

などとします。

2つのconfigは、これもおまじないみたいなものですね。以前の経験から習慣になっているんですが、果たして何で必要だったかは忘れてしまいました……すみません。

最後のpushですが、origin masterの部分は、最初の一回だけです。ブランチ分けたり、機能を十全に使いこなすほどでもなければ、以降はgit pushだけで十分です。

まとめ

今回は、ほとんど導入だけを記しました。

今後、ブランチの切り分けなど、まともに使えるようになった際には、またこうしてメモを書くかと思います。

それでは、この場はお開きということで。

おまけ

Gitを使ってリポジトリをたくさん作っていたら、それらのリポジトリの状態を一斉に知る手段が欲しくなりました。

んで。

作ったシェルスクリプトが、以下になります。

#!/bin/sh

case `uname` in
    FreeBSD)
        ECHO="echo -e"
        ;;
    *)
        ECHO="/bin/echo -e"
        ;;
esacgit pullにまでsudoが必要になってしまいます。

status_check(){
    case `$ECHO $1|cut -d " " -f3,4|sed "s/ //"` in
        Permissiondenied)
            ;;
        *)
            if test -d $1 ;
            then
                cd $1
                if test -e .git ;
                then
                    $ECHO "\033[01;35m$1: git status\033[m"
                    git status
                    cd -
                else
                    :
                fi
            else
                :
            fi
            ;;
    esac
}

$ECHO "\033[01;31m\c"

for i in `find .|grep -F ".git/config"|sed "s/\.git[\/a-zA-Z]*//"` ;
do
    $ECHO "\033[m"
    status_check $i ;
done

これを、GitStatusCheckなどと名づけて、僕は使っています。

機能は、カレントディレクトリ以下に存在するGitリポジトリを、.git/configの有無によって捜索・判定し、Gitリポジトリであるならばgit statusを順次実行するというものです。

たとえば、hogeというユーザーが/home/hogeでこのスクリプトを実行すると、ホームディレクトリ以下にあるリポジトリを捜索し、その状態を吐き出すというものです。

ですから、下手に/(ルートディレクトリ)なんかで実行すると酷い目に合います。用法・用量は正しくお使いください、です。

訂正(2011/04/12)

おまけのシェルスクリプトに関して、

for i in `find .|grep -F ".git/config"|sed "s/.git[\/a-zA-Z]*//"` ;

を、

for i in `find .|grep -F ".git/config"|sed "s/\.git[\/a-zA-Z]*//"` ;

に訂正しました。

以前のものだと、たとえば、git/local-repositoryみたいにしていた場合、再帰的な処理に引っ掛かりません。バックスラッシュ一個の違いは、甚だ大きい、略して甚大です。

また、

case `uname` in
    FreeBSD)
        ECHO="echo -e"
        ;;
    *)
        ECHO="/bin/echo -e"
        ;;
esac

の部分は、お使いのシェルなどの環境による挙動の違いが大きいので、適宜変更する必要があります。
自分の環境でしか試していないので、環境によってどのような影響があるか把握しきれていないことを、ご了承願いたいと思います。

失礼しました。

東日本大震災

早くも1ヵ月が経過しようとしていますが、未だに事態は現在進行形で予測不能の状態にあり、被災地の方々も不安で仕方がないのではないかと思います。

地震津波という天災で壊滅的な被害に加えて、原子力発電所の非常事態という深刻な問題が、今回の震災を強烈に印象付けています。特に、日々報道されている原発の問題に関しては、世界中からその動向を注視される大問題に発展しています。自分の身近でそんな状況が繰り広げられていたら、平時であっても落ち着いてはいられないでしょう。

ところで、今回の震災で、僕は改めて日本という国が、狭い国土の中でギリギリの努力をしていたことを痛感しました。電力のことは言うまでもありませんが、たとえば他にも、シリコンウエハーの生産工場が被災し、世界の半導体生産に大打撃を与えるかもしれないという話。スケールの大きさに、驚いてしまいました。

どうなんでしょうね。首都が繁栄するから地方に潤いが分配されるのか、地方の支えがあるから首都の繁栄が成り立つのか。たぶん、そのどちらもなんでしょうけれど、今回の出来事で後者の影響がより前面に押し出された感を個人的には抱いています。

ともあれ、災害が終わらないことには復興の目処も立たないでしょうし、個人的にも不安なのが正直なところなので、一日でも早く原子力発電所の問題が収束することを願っています。

GAE Tips Vol.2

なんて書いてますけど。
要するに、ただのメモです。
相変わらずっちゃあ、相変わらずですが。
『Tips』って聞くと、イメージ的には粋な小技って感じ(※感想には個人差があります)なので、タイトルに対してどうしようもないメモしか書かないことに抵抗が無い訳ではないのです。


て言うか、白状します。
あまりにも久しぶり過ぎて、記事の書き方忘れました
そんな訳で。
過去の記事を参照しながら書いてます。
いいですね、こういうの。
過去に自分が積み上げたものが、現在の資産となって活かされる。
素晴らしいじゃないですか。
まあ、積み上げたのが僕なので資産というよりも負債かもしれませんが。


さて、本題ですね。ネタの一つ一つが細かいので、特に分けないでだらだら書いてこうと思います。
ちなみに、久方ぶりなので自己確認の意味も込めて書き記しますが、このメモは『僕自身がわからなくて調べたことのまとめ』です
なんで、赤字なんでしょう?
シンキングタイム、ボーナス10ms(マイクロセカンド)!
はい、その通り!正解は『警告の意も兼ねているから』でした!
………………。


端的に言うと、オープンであることはオマケみたいなもので、ウェブ上のテキストファイルのストレージみたいな感覚で僕はこの日記を書いているんです。
なので、貴方がここの記事でどれだけ時間を無駄にし、不快感を催したとしても、その責任を負いかねると胸を張って言っている訳です。
そもそも、日記って人に見せるためにかくもんじゃなくね?とか。
見せるための日記っていうのが新鮮だったんじゃんか!とか。
そういう議論はしないで下さい。
僕のために争う人を見たくない……ああ、僕のためではないですか、そうですか。


それでは、サボりにサボったので、最終的にはとんでもなく長くなるかもしませんので、退場はセルフ・サービスでお願いします。
オープンとクローズがセルフでないページが限りなくマイノリティですが。

管理者だけログイン

初っ端からとんでもなく、ちっちぇネタです。
まあ、これはソースだけ載せときます。(うわ……ソースの載せ方まで忘れてる……)

-  url: /admin/.*
   static_files: htdocs/admin.html
   upload: htdocs/admin.html
   login: admin

これで、該当ページのオープンはログインを要求され、管理者でなければ弾かれます。


ここで、uploadなんですけど、これがいまいちよく分からない。Googleのドキュメントも見たんですが、理解があってるのかどうか……。
でも、要するに『URLとアプリケーションディレクトリにおけるファイルパス(サーバー上のファイル位置)の一致を補助する』ためのものだと思います。


そもそも、GAEでは効率化のため(と仰ってます、Google様が)に、アプリケーションファイル(スクリプトファイルって言ってもいい?)と静的ファイルが別々に保存されています
余談ですが、その都合上アプリケーションファイルからアクセスする必要のあるデータ用のファイルは静的ファイルとして扱われないように、アプリケーションファイルとしてサーバーにアップロードする必要があるそうです。
加えて、アプリケーションファイルと静的ファイルがサーバー上でそれぞれ異なるファイルシステムで管理されています。
気にしないで使ってましたけど、けっこう芸が細かいんです。
まあ、世の中大抵そんなもんですよね。見えない努力の積み重ねが、日常を、当たり前を支えているんです。きっと、僕の努力もいつか。
そのために、まずは努力することから始めようと思います。明日くらいから。


で、話逸れましたが。
要するに、uploadが必要なのは、static_filesハンドラの場合のみです。
どういうことかって?
それは簡単には教えらんねぇなぁ。
だって、僕もよくわかってないんだもん!
……いえ、冗談です。


たとえば、

-  url: /admin
   static_dir: admin

とした場合、ローカルにおけるadminというディレクトリ内のファイルは、一括して静的ファイルとして扱われます。つまり、このディレクトリ内にアプリケーションファイルを配置することはできません。(おいても、静的ファイルとして扱われることでしょう。)


一方、再掲となりますが、

-  url: /admin/.*
   static_files: htdocs/admin.html
   upload: htdocs/admin.html
   login: admin

と書いた場合は、特に他で設定してない限り、htdocsは静的ファイルもアプリケーションファイルも混在可能です。それは、static_filesハンドラ、つまり静的ファイルパターンハンドラが、ディレクトリ全体ではなく、ファイル単位でURLとマッピングするからです。
uploadはここで活きてくるのです。
つまり、そもそも静的ファイルとアプリケーションファイルが別々で保存されているのに、『このURLはこのファイルね』とか言われても、ハンドラはどっちのファイルシステムを探してよいかわかりません。この対応関係を教えてあげるのが、uploadなのです。


もっと端的に言うならば、uploadは対象が静的ファイルであることを明言するためにあります。
ですので。
必然、static_filesとuploadはセットで使うことになる。
唐突にアルス=マグナ発動した訳ではないです
ともあれ。
書かなくても動く可能性ありますが、すみません、今回はそこまでの検証実験は行ってません。
ただ、書いた方が安心、丁寧ということはあるでしょう。

あれ?これ、何を書こうと思った項目だったっけ?

VS. GQL!日付で条件を操作せよ!

これ、何かご存知の方がいたら、ぜひご教授いただきだい内容です。


実は、この空白期間に、ちょっとしたプライベートなチャットシステムを作ったんです。
ラクタも同然の代物でしたけど。
んで。
チャットなんで、書き込みはどんどん増えます。利用者が二桁もいかない程度だったので、まだ良かったんですが、それでも日に50件は書き込みがあるものでした。
だから、データストアへのアクセスもたくさんあったんですよ。


また余談ですけど、データストアへのアクセスは非常にCPUリソースを食います。
気を抜くと、あっという間にクオータの上限越えます。
これは使用率の話ですが、だいたい80件くらい呼び出そうとすると、ダッシュボード(GAEアプリの管理コンソール)に警告メッセージが表示されます。黄色で。
100とかいくと、赤いアイコンで警告されます。
まあ、そこは何とか60件表示して、次があったらリンクを表示して、リンクがクリックされたら次の60件を表示するっていう方法をとり、何とか収めましたが。
これは、別件なので割愛。


で、本題に戻りますけど、アクセスの問題の他にもGQL、つまりデータストアへのアクセス手法でも一つ僕が悩みに悩んだものがあります。
それが、日付での絞り込み検索です。
たとえば、

query = Greeting.gql('WHERE date <= :1 '
                               'ORDER BY date DESC',
                               '2011-01-01 00:00:00')

とかってしたいとしましょう。
この時、dateがdb.DateTimePropertyで宣言されていると比較できなかったんです……。


まあ、ドキュメントをよく見れば書いてあるんだと思います。
でも、僕はそれを見つけられなくて、「何で比較できる形にしてあるのに結果が違うんだ!?」とか「これはGoogleの陰謀か!?」とか、無駄に騒いだものですが。
結局。
新たに日本時間で保存するjdateをdb.StringPropertyで宣言して作ることで、検索可能になりました。
上記のようにjdateを宣言し、先ほどのGQLを、

query = Greeting.gql('WHERE jdate <= :1 '
                               'ORDER BY jdate DESC',
                               '2011-01-01 00:00:00')

とすると、欲しかった検索結果が表示されるようになったのです。


この経験から僕は、『日付で条件付き検索を行いたいなら、日付をStringProperty、つまり文字列型として保存するのが良い』、と思っているんですが、どうなんでしょうね?
ちなみに、ドキュメントでは比較可能な形の中に『2011-01-01 00:00:00』を挙げていますが、『2011/01/01 00:00:00』のようにハイフンをスラッシュで保存して検索しても構いません。
ちゃんと絞り込み検索可能です。
ソースは僕です。
信憑性の欠片もありませんね。

結び

いやあ、もっと書くことあるはずなんですけどね。
力尽きました。
と言う訳で、残りは次回に持ち越しです。
まあ、一つの記事が長くなりすぎるのも嫌なので、結果オーライということにします。
しかし、クラウドのスケーリングの醍醐味を味わおうと、データストアへのアクセス、つまり先述のチャットシステムにおける過去コメントの表示を1000件に設定して、書き込みログが増えた途端に警告祭になったのには驚きました。
ほんとに。
最初は書き込みのログも少ない、つまりアクセスしても読み込む件数が少ないから、1000件設定でも問題なかったんですが、すぐにとんでもない事態になってしまって……。
ダッシュボードをこまめに見ていなかったら、どうなってたんでしょうねぇ……。
まあ、どうなってたんだろうって言うか、結局アプリケーションが停止するんでしょうけどね。
60件ずつ表示とか……超ちまちましてて面倒くさいよね……。
って、使ってた方は思ってたことでしょう。
うん、ごめんなさい。


こんな感じで。
今日のところは、これでご寛恕願います。
それでは、またー。