レポジトリ・trac 生成 CGI を作る

先日の続きです。SVNParentPath と TracEnvParentDir を活用してないととくにおいしくない話です。

先日のように私用環境では Subversion のレポジトリと Tracディレクトリの owner は apache にしてあります。このままだと新規レポジトリを作成したりするのにいちいち su apache になったりしなきゃいけないのが面倒なので,じゃあいっそウェブアプリ化してみよう,ということです。

アプローチ

ぶっちゃけ Python のコードを読み書きしたのは今回が初めてなので見よう見まねです(全然 try 等で例外をキャプチャしてねー)。

Python には CGI, FastCGI, mod_python 等の差異を吸収してくれる WSGI というインタフェースがあるのですが(WSGI については後述のリンク先にある id:tokuhirom さんの記事が詳しい),Trac ではさらにその上に CGIGateway, ModPythonGateway 等のラッパが用意されていたのでそれらを再利用しました。

Subversion のレポジトリツリーは,かつて記述したように svn.repos.create(ほげほげ, ...) で作成します。

trac-admin コマンドまわりは機能単位でコンポーネント化されているのかと思ったら,trac.scripts.admin.py にだらだらと書かれていたんで,標準出力・標準エラー出力をトラップする必要がありました。

インストール

ソースツリーは

+-+- setup.py
+-+- trac_webadmin/
  +--- main.py
  +--- modpython_fe.py

こんな感じで,

% sudo python setup.py install

とかすると site-packages 以下にインストールされます。

先日の trac.conf に以下のように付け加えます。

<Location /trac/admin/create>
#   SetHandler mod_python
    PythonHandler trac_webadmin.modpython_fe
#   PythonOption TracEnvParentDir      /var/www/trac
    PythonOption TracEnvReposParentDir /var/www/repos
</Location>

mod_python と TracEnvParentDir は先日の例だと上位 location で設定されているので,ここの例ではそれらを継承したとみなしてコメントアウトしてます。

ソース

trac_webadmin/main.py は以下の通り

# -*- coding: utf-8 -*-

import os
import sys
import cgi
import svn.core
import svn.repos
from trac.web.api import *
from trac.scripts.admin import TracAdmin
from trac.config import default_dir

def escape_quot(s):
    return s.replace('"', '\\"')

class StreamSweeper:
    def __init__(self):
        self.stock = ''
    def write(self, s):
        self.stock += s

class App:
    def __init__(self, req):
        self.req = req

    def run(self):
        if self.req.method == 'POST':
            self.action_create()
        else:
            self.show_form()

    def action_create(self):
        req = self.req
        if req.args['repos'] == '' or req.args['project'] == '':
            return self.show_error('Invalid Parameters')

        path_repos = req.environ.get('trac_webadmin.env_repos_parent_dir') \
                     + '/' + req.args['repos']
        path_trac  = req.environ.get('trac_webadmin.env_parent_dir')       \
                     + '/' + req.args['repos']

        try:
            svn.repos.create(path_repos, '', '', None, None)
        except Exception, e:
            return self.show_error( 'svn.repos.create failed: %s' % e )

        last_stdout = sys.stdout
        sys.stdout = StreamSweeper()
        last_stderr = sys.stderr
        sys.stderr = StreamSweeper()

        log_stdout = ''
        r = 0
        succeeded = False

        try:
            admin = TracAdmin()
            admin.env_set(path_trac)
            r = admin.do_initenv( '"%s" %s %s "%s" "%s"' %
                ( escape_quot(req.args['project']),
                  'sqlite:db/trac.db',
                  'svn',
                  escape_quot(path_repos),
                  escape_quot(default_dir('templates')) ) )

            log_stdout = sys.stdout.stock
            succeeded = True
        finally:
            sys.stdout = last_stdout
            sys.stderr = last_stderr

        if succeeded:
            if r > 0:
                self.show_error(log_stdout)
            else:
                self.show_created(log_stdout)
        else:
            self.show_error('TracAdmin.do_initenv failed')

    def _send_headers(self, status = 200):
        req = self.req
        req.send_response(status)
        req.send_header('Content-Type', 'text/html; charset=UTF-8')
        req.end_headers()

    def show_form(self):
        self._send_headers()
        self.req.write("""
<html>
    <head>
        <title>プロジェクトの作成</title>
        <style type="text/css">
            th { text-align: left; }
        </style>
    </head>
    <body>
        <h1>プロジェクトの作成</h1>
        <form method="post">
            <table>
                <tbody>
                    <tr>
                        <th>レポジトリ名</th>
                        <td><input type="text" name="repos" /></td>
                    </tr>
                    <tr>
                        <th>プロジェクト名</th>
                        <td><input type="text" name="project" value="My Project" /></td>
                    </tr>
                    <tr>
                        <th></th>
                        <td><input type="submit" /></td>
                    </tr>
                </tbody>
            </table>
        </form>
    </body>
</html>
        """)

    def show_created(self, log_stdout = ''):
        self._send_headers()
        self.req.write("""
<html>
    <head>
        <title>プロジェクトの作成 - 成功</title>
    </head>
    <body>
        <h1>プロジェクトの作成</h1>
        <p>成功しました</p>
        <pre>%s</pre>
    </body>
</html>
        """ % ( cgi.escape(log_stdout).encode('utf-8') ) )

    def show_error(self, msg = ''):
        self._send_headers(500)
        self.req.write("""
<html>
    <head>
        <title>プロジェクトの作成 - エラー</title>
    </head>
    <body>
        <h1>プロジェクトの作成</h1>
        <p>エラーが発生しました</p>
        <pre>%s</pre>
    </body>
</html>
        """ % ( cgi.escape(msg).encode('utf-8') ) )

def dispatch_request(environ, start_response):
    if 'mod_python.options' in environ:
        options = environ['mod_python.options']
        environ.setdefault('trac_webadmin.env_parent_dir',
                           options.get('TracEnvParentDir'))
        environ.setdefault('trac_webadmin.env_repos_parent_dir',
                           options.get('TracEnvReposParentDir'))
    else:
        environ.setdefault('trac_webadmin.env_parent_dir',
                           os.getenv('TRAC_ENV_PARENT_DIR'))
        environ.setdefault('trac_webadmin.env_repos_parent_dir',
                           os.getenv('TRAC_ENV_REPOS_PARENT_DIR'))

    req = Request(environ, start_response)
    app = App(req)
    app.run()
    return req._response or []

trac_webadmin/modpython_fe.py は以下の通り。

# -*- coding: utf-8 -*-

from mod_python import apache
from trac.web.modpython_frontend import ModPythonGateway
from trac_webadmin.main import dispatch_request

def handler(req):
    options = req.get_options()
    gateway = ModPythonGateway(req, options)
    gateway.run(dispatch_request)
    return apache.OK

setup.py は以下の通り。

#!/usr/bin/python
# -*- coding: utf-8 -*-

from distutils.core import setup

setup(name='trac-admin web interface',
      version='0.1',
      packages=['trac_webadmin'])