« 2008年9月 | トップページ | 2008年12月 »

2008年11月

2008年11月29日 (土)

rubyでscpによるリモートホストのファイルのバックアップ

仕事でいつも活用している自作のバックアップツールをrubyforgeに
登録したけど、私以外は誰も使っていないようだ。
remote ssh backup
どういうツールかというと、サーバー等のリモートホスト上にある
ディレクトリをローカルにscpでコピーすることでバックアップを
行う。しかも、ファイルの更新日時を見て、初回以外は更新されて
いるファイルだけをコピーしてきて、リストアする際に日付を指定
して、指定された日付の状態をリストアできる。
このプログラムはたかだか500行程度のrubyでできている。
リモートサーバのバックアップはrsyncが一般的だと思うが、過去の
データをとれないから、間違ってサーバのファイルをいじった時に
元にもどせない。

インストール手順
 sudo gem install remotebackup

バックアップ手順
1.まず、下記のフォーマットでバックアップを行うための設定ファイル
を作成する。デフォルトはカレントディレクトリのbackup.xmlという名前。

<backups>
  <backup>
    <name>バックアップ名(適当な英数字)</name>
    <server>ホスト名</server>
    <user>ホストにログインできるユーザ名</user>
    <password>ユーザのパスワード</password>
    <path>バックアップしたいディレクトリのフルパス</path>
    <ignore_list>
      <ignore>無視したいディレクトリおよびファイルの正規表現</ignore>
                      :
    </ignore_list>
  </backup>
        :
</backups>


<backups>
    <backup>
        <name>web</name>
        <server>192.168.0.10</server>
        <user>takesy</user>
        <password>openthesesami</password>
        <path>/www</path>
        <ignore_list>
            <ignore>\.svn</ignore>
            <ignore>\.bk</ignore>
        </ignore_list>
    </backup>
    <backup>
        <name>nfs</name>
        <server>192.168.0.12</server>
        <user>bkuser</user>
        <password>passforbkuser</password>
        <path>/export</path>
        <ignore_list/>
    </backup>
</backups>

2. 下記のコマンドを実行することにより、バックアップ開始.
remote_backup [-f 設定ファイル。デフォルトはbacup.xml] [-o 出力したいディレクトリ デフォルトはカレントディレクトリ] [-v どのファイルをコピーしているか等の情報を表示]

上記実行により、レポジトリとしてカレントディレクトリもしくは-oで指定したディレクトリに<name>で指定したディレクトリが作成される。
上記例だとwebとnfsという名前のディレクトリができる。
その中にバックアップされたファイルとさらにファイル情報として、実行日時.ymlというバックアップしたファイルの属性等が記述されたファイルが作成される。
このファイルはリストアする時に使用する。
また、前回のバックアップ時と比較して変更がなかった場合は作成されない。
------------------------------------------------------------------------------
リストア手順

1.リストアするためには下記のコマンドを実行する。
restore_backup -f file -o output_dir [-v vervose]

fileには、バックアップで作成された日付+ymlを指定する。
output_dirにはリストアしたいディレクトリのパスを指定する。

例:
restore_backup -f ./web/2008_06_25_06_40.yml -o /tmp/www

------------------------------------------------------------------------------

既知のバグ
1. ファイル、ディレクトリ名に日本語等のマルチバイトを使用している
場合は強制的にバックアップされない。

理由:ファイル名によっては、scpでエラーが出てしまうため、プログラム
強制的に除外している。(s-jisのみ?)別に問題がないようだったら、
その部分をコメントアウトしてください。

2. 30Mを越えるファイルがあるとエラーになる。

理由: プログラムで使用しているnet/scpのバグだと思います。他の
ブログでも同じことを言っていたから.net/scpの修正を待つか、以前
net/scpが登場する前に自作したscpの処理を持ってくれば対応できる
はず。

| | コメント (0) | トラックバック (0)

google app engine 家計簿 ソース公開

グラフ機能やメモ機能を追加したため、家計簿のソースが大幅に
変わったため、ソース自体をgoogle codeとして下記に公開しました。
結局家計簿をwebアプリにしても、管理者にデータを見られるため、誰も
使いたがらないので、それなら、自分専用のwebアプリにしてしまえと
いうことです。google app engineだったら、無料でwebアプリを作成できるし。

http://code.google.com/p/accountnote/

プログラムの説明は見れば、大したプログラムでもないので見ればわかると
思うし、google app engine自体を私は詳しいわけではないので、やっぱり
説明するのは打ち切ります。質問があれば、コメントしていただければ答えます。

| | コメント (0) | トラックバック (0)

2008年11月14日 (金)

google app engineで家計簿3

accountList.js

var AccountList = Class.create({
  initialize:function(elem,type,form)
  {     this.type=type;
    if(this.type == "list")
    {       this.url="/accountList.json";
    }     else     {       this.url="/account.json";
    }     this.elem = $(elem);
    this.form = $(form);
  },
  loadData:function()
  {     var json=null;
    var a = new Ajax.Request(
        this.url,
        {             "method": "get",
            "parameters": Form.serialize(this.form.id),
            onSuccess: function(request) {               eval("json="+request.responseText);
              this.makeAccountList(json);
            }.bind(this),
            onFailure: function(request) {                 alert('読み込みに失敗しました');
            },
            onException: function (request) {                 alert('読み込み中にエラーが発生しました');
            }         }     );   },
  makeAccountList:function(dataSet)
  {       this.elem.innerHTML=""       var total = 0;
      var tableStr = "";
      if(dataSet.length != 0)
      {         tableStr += '<table id="account_list">';
        if(this.type!="list")
        {           tableStr += '<thead><tr><th>項目</th><th>金額</th><th>内容</th><th>機能</th></tr></thead><tbody>';
        }         else         {           tableStr += '<thead><tr><th>日付</th><th>項目</th><th>金額</th><th>内容</th></tr></thead><tbody>';
        }         for(var i=0;i<dataSet.length;i++)
        {

          var data = dataSet[i];
          tableStr +=
'<tr>';
         
if(this.type=="list")
         
{
            tableStr +=
'<td  width="100px">'+data.year+'/'+data.month+'/'+data.date+'</td>';
         
}
          tableStr +=
'<td class="bold" width="120px">'+data.category+'</td>';
          tableStr +=
'<td width="80px" style="text-align:right">'+myFormatNumber(data.price)+'円</td>';
          tableStr +=
'<td width="400px">'+data.content+'</td>';
         
if(this.type!="list")
         
{
            tableStr +=
'<td style="vertical-align:bottom">';
            tableStr +=
'<input class="deleteOfKey" type="submit" value="削除" id="'+data.key+'" /></td>';
         
}
          tableStr +=
'</tr>';
          total += data.price;
       
}
        tableStr +=
"</tbody></table>";
      
}
      tableStr +=
"<hr/> <div>合計 "+ myFormatNumber(total) + "円";
      
this.elem.innerHTML = tableStr;
      $$(
".deleteOfKey").each(function(elem){
      elem.observe(
'click',
      
function(event){this.deleteAccount(event.element().id);}.bind(this))}.bind(this));
 
},
  deleteAccount:
function(key)
 
{
   
var a = new Ajax.Request(
       
"/deleteAccount",
       
{
            
"method": "post",
            
"parameters": "key="+key,
            onSuccess:
function(request) {
             
this.loadData();
            
}.bind(this),
            onFailure:
function(request) {
               
alert('読み込みに失敗しました');
            
},
            onException:
function (request) {
               
alert('読み込み中にエラーが発生しました');
            
}
       
});
   
return false;
 
}
});


メインページ、一覧画面でも使うので、typeによって、メインページか、一覧画面かを分けているのは
昨日説明した通り。
loadDataをinitializeで呼ばないのは、検索画面で検索条件を設定するまで表示させないようにするため。
loadData呼び出しにより、家計簿データをjsonで取得し、makeAccountListでテーブル一覧として
出力している。メインページでは削除できるように削除ボタンを出し、検索画面では、各レコードの日付を
出力している。
      $$(".deleteOfKey").each(function(elem){
      elem.observe('click',
      function(event){this.deleteAccount(event.element().id);}.bind(this))}.bind(this));
はdeleteOfKeyクラスのボタンに対してクリックされるとボタン作成時にボタンのidとして、該当するレコードの
キーを値としてセットしていたので、idを取得することでキーを指定して、削除要求をサーバに
出している。google app enigineのデータの面白いところは、各レコードはidではなく、keyという値を
持ち、それはテーブルではなく、データベースで一意であるということである。
db.get(db.Key(key))で文字列のkeyを実際のkey値にした値をdbに渡せばレコードを取得できる。
bind(this)をしつこくしているのは、イベントハンドラのthisは通常そのエレメントがthisになってしまうが、
bind.thisにすることによって、accountListのインスタンスをthisにできるからである。そうでないと、accountList
クラスのdeleteAccountを呼び出せない。

| | コメント (0) | トラックバック (0)

2008年11月13日 (木)

google app engineで家計簿作り2

今日は最初にgoogle app engineのgvimの設定から。

function GoogleUpload()
  let s:nowDir = substitute(expand("%:p:h"),"c:\\app\\(.*\\)$","\\1","")
  execute "!python \"C:\\Program\ Files\\Google\\google_appengine\\appcfg.py\" update  ".s:nowDir
endfunction
cnorem <C-e> call GoogleUpload()<CR>

これが何がうれしいかというと、google app engineのソースを修正している際に
ノーマルモード状態から: 入力によりコマンドモードに移った後、 Ctrl+e入力で
現在のソースがあるディレクトリに対してappcfg.py upload を実行できるという
ものだ。
コマンドプロンプトを立ち上げておいてもいいのだが、appcfg.pyのフルパスを
入力するのが面倒だし、エディタから離れずにアップロードができるのは、
連続的にできるため、思考が途切れない。
普段の開発でもよく、svcコマンドやftpコマンドをこのようにキーマップに割り当てて
開発する。
コマンドモードに対してキーマップを割り振るのは比較的コマンドモードのキーは
割り当てられていないから、バッティングしないからである。

index.html

<html>
  <head>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <title> 家計簿 </title>
 <script type="text/javascript" src="js/prototype-1.6.0.3.js"></script>
 <script type="text/javascript" src="js/common.js"></script>
 <script type="text/javascript" src="js/dateselect.js"></script>
 <script type="text/javascript" src="js/accountList.js"></script>
 <link href="css/account.css" rel="stylesheet" type="text/css">
 </head>
<body>
  <div>
    <input type="submit" value="項目編集" 
            onclick="
document.location.href='/category';"/>
    <input type="submit" value="一覧" 
           onclick="
document.location.href='/listaccount';"/>
  </div>
  <hr/>
  <div class="bold" id="totalAccount"></div>
  <form id="account" action="/" method="post">
    <div id="account_date">
    </div>
    <div>
      <span class="title" id="items_span"></span>
    </div>
    <div>
      <span class="title">費用</span>
      <input type="text" name="price" id="account_price" size="9"
                     
style="{ime-mode : disabled}"/>
    </div>
    <div><span class="title">内容</span>
      <input type="text" size="100" id="account_content"
               
name="content" style="vertical-align:top"/>
    </div>
    <input type="submit" value="送信" onclick="return dataSend()"/>
  </form>
  <div id="accountTitle"></div>
  <div id="accountTable"></div>
  <script type="text/javascript">
 function dataSend()
 {
 var a = new Ajax.Request( 
 "/addAccount",
 { 
 "method": "post",
 "parameters": Form.serialize("account"),
              onSuccess: function(request) { 
                $('account_content').value=""
                $('account_price').value=""
                setAccountData();            
 },
              onFailure: function(request) { 
 alert('読み込みに失敗しました');
 },
              onException: function (request) { 
 alert('読み込み中にエラーが発生しました');
 } 
 });
 return false;
 }
 function setAccountData()
 {
      setTotalAccount();
 var dateStr = $F('account_year')+"年"+$F('account_month')+"月"
                      
+$F('account_day')+"日";
 var tableStr = '<h3>'+dateStr+'の費用一覧 </h3>';
      $('accountTitle').html = tableStr;
      accountData.loadData();
 } 
 function setTotalAccount()
 {
 var a = new Ajax.Request( 
 "/gettotalaccount",
 { 
 "method": "get",
 "parameters": Form.serialize("account"),
              onSuccess: function(request) { 
                $('totalAccount').innerHTML=$F('account_year')+'年'+
         $F
('account_month')+'月の合計:'+
                  myFormatNumber(request.responseText) + '円';
 },
              onFailure: function(request) { 
 alert('読み込みに失敗しました');
 },
              onException: function (request) { 
 alert('読み込み中にエラーが発生しました');
 } 
 });
 }
 function setCategoriesData(categories)
 {
 if(categories.length == 0)
 {
 alert("初めてのご使用ですね。項目編集のボタンを押下して、" +
       "食費、交通費等の項目を設定してください。"
);
 return;
 }
      optStr = '項目<select name="category" id="category">';
 for (var i=0 ;i<categories.length;i++)
 {
        optStr += '<option value="'+categories[i].key+'" >'+
                                 categories
[i].name+'</option>';
 }
      optStr+='</select>';
      $("items_span").innerHTML = optStr;
 }
 function setCategories()
 {
 var a = new Ajax.Request( 
 "/getcategories",
 { 
 "method": "get",
              onSuccess: function(request) { 
                setCategoriesData(eval(request.responseText));
 },
              onFailure: function(request) { 
 alert('読み込みに失敗しました');
 },
              onException: function (request) { 
 alert('読み込み中にエラーが発生しました');
 } 
 });
 }
 var dateSelector = null;
 var accountData = null;
 document.observe('dom:loaded',function(){
    accountData = new AccountList('accountTable',"day","account");
    dateSelector = new DateSelector('account','account_date',
                                    setAccountData
);
    setCategories();
 });
 </script>
</body>
</html>

ではメインページのhtmlに移る。
昨日説明したとおり、テンプレートとしてindex.htmlを呼び出しているが、変数による
置き換えがひとつもない。
データはすべてajaxによってjson形式でサーバから取得してそのデータに基づいて
表示している。

jsはprototype.js以外は自作のjsファイルだ。
pythonを使う人は比較的jQueryを使う人が多いのだが、私はprototypeの方が
bind関数やquery stringのパース、htmlのエスケープ関数が最初から用意されていて
ありがたい。
document.observe('dom:loaded',function(){}はprototype.jsの常套句で、htmlのdom
が一通りパースされ、すべてのelementに対してイベントが登録できる状態(イメージの
読み込み等はまだ)になったときに呼び出されるイベントハンドラである。
accountData = new AccountList('accountTable',"day","account");
は AccountList(一覧を表示するdivのID,一覧の種類,jsonを取得する際に渡すデータの
元となるformのID)のオブジェクト呼び出しである。
一覧の種類は今のところ"day"と"list"があり、メインページで使用する"day"は当日
使った費用の一覧で、日付は自明のため出力せず、データを削除するためのボタン
は出力する。
一覧画面で出す"list"は日付はまたがるため出力し、削除ボタンは出力しないという
違いがある。
dateSelector = new DateSelector('account','account_date',setAccountData);は
DateSelector(jsonを取得する際に渡すデータの元となるformのID,日付選択のselectを
作成するdivのID,日付が変わった際に呼び出すコールバック関数)のオブジェクトを
作成している。
setCategories()はajaxで項目一覧(食費や交通費)のjsonを取得し、ID items_span
のdivの中に項目を選択できるselectを作成している。

各jsは明日以降説明する。

| | コメント (0) | トラックバック (0)

2008年11月12日 (水)

google app engineで家計簿作り

せっかくdjangoに慣れ親しんだから、google app engineを使わない手は
ないっしょということで家計簿ソフトを作成。

実際に使ってみたい人はhttp://lifeaccountnote.appspot.com/ にアクセス。
使い方は簡単で項目編集で食費、家賃など分類したい項目を作成した後
家計簿画面で項目、金額、内容を入力すれば、その日使ったデータの一覧
が下に表示される。また、一覧画面では何月何日~何月何日までの指定と項目
の指定(無指定も可)をすることで、条件に一致したデータの一覧と合計を出力する。

メイン画面のイメージ

Main

まずは設定ファイルであるapp.yamlから。


# アプリケーションのID

application: lifeaccountnote

# アプリケーションのバージョン
version: 1

# ランタイムの名称。現時点では「python」のみ
runtime: python

# アプリケーションが前提としているAPIのバージョン
api_version: 1

# URLと、その処理方法の定義
handlers:
- url: /js
  static_dir: js
- url: /css
  static_dir: css
- url: /.*
  script: lifeaccountnote.py

urlのところの
- url: /js
  static_dir: js
の意味はパスが/jsで始まったら、google app engineのアプリを起動せずに
jsディレクトリ配下のファイルを直接返しますよという意味。
url:/.*はそれ以外のパスはlifeaccountnote.pyというgoogle app engine
用のpythonプログラムを起動させますという意味。

lifeaccount.py (最初の画面のみ)
# vim: fileencoding=utf-8
#!-*- coding:utf-8 -*-
#日本語
import cgi
import logging
from django.utils import simplejson
import os
import wsgiref.handlers
import datetime
from google.appengine.api import users
from google.appengine.ext import db,webapp
from google.appengine.ext.webapp import template
from google.appengine.ext.webapp.util import login_required

class MainPage(webapp.RequestHandler):
  @login_required
  def get(self):
    path = os.path.join(os.path.dirname(__file__), 'index.html')
    self.response.out.write(template.render(path, {}))

  def post(self):
    user = users.get_current_user()
    _add_account(self,user)
    self.redirect("/")
def main():
  logging.getLogger().setLevel(logging.DEBUG)
  application = webapp.WSGIApplication(
                                       [('/', MainPage),
                                         ],
                                       debug=True)
  wsgiref.handlers.CGIHandler().run(application)
if __name__ == "__main__":
  main()

path = os.path.join(os.path.dirname(__file__), 'index.html')
は常套句。google app engineにアップロードしてもどこに配置されるか
判らないので__file__で今のファイルのフルパスを取得して、その
ファイルが置かれているディレクトリにテンプレートとして使用する
index.htmlのパスをpathにセットしている。
self.response.out.write(template.render(path, {}))
はtemplateにデータを書き込んだものをクライアントに返却しますよと
いう意味で、pathには先ほどのテンプレートのフルパスが入り、その後
テンプレートに渡すハッシュを渡すのだが、今回はAjaxでページを作成
するため、テンプレート自体をそのまま返却するため、空ハッシュを渡
している。だったらstaticのディレクトリに置けばいいのかも。

今日はこの辺で。

| | コメント (0) | トラックバック (0)

« 2008年9月 | トップページ | 2008年12月 »