読者です 読者をやめる 読者になる 読者になる

UnfuddleAPIのラッパーとSVNバックアップ機能をRubyで書いてみた

Ruby Unfuddle コード

UnfuddleSVNバックアップを自動化したかったので、UnfuddleAPIのラッパーとSVNバックアップをRubyで書きました。
晒したコードは自分の記憶のためとメモを兼ねてます。
かなり中途実装なので、突っ込みどころがたくさんあると思います。
最低限自分がしたい事は実装したので、APIは一部分しかラップしていません。(^^;

現在動作できるのはたぶんWindowsのみです・・・*1

準備

依存しているJSONとかTmailなどのパッケージをgemsを使ってインストールする。

基本的な使い方(Revが上がっていたらSVNのバックアップを行う)

UnfuddleAPIWrapperを読み込み、パラメータを用意して、インスタンスを作ります。
最低限必要なパラメータは「unfuddle_settings」だけです。
「backup_settings」パラメータがない場合は、svnbackupメソッドが使用できません。*2

require 'unfuddleapiwrpp'

# --------------------------------------------------------------
# 【パラメータ】
# 1.Unfuddleに関する設定
# 2.バックアップ出力に関する設定 ※任意((SVNリポジトリBACKUP機能を使う時は必要))
# --------------------------------------------------------------
# Unfuddleに関する情報
settings = {
	:unfuddle_settings => {
		:subdomain  => 'hoge',                                    #アカウント:サブドメイン
		:username   => 'hoge',                                    #アカウント:ユーザー名
		:password   => 'passwordxx',                              #アカウント:パスワード
		:url        => 'http://hoge.unfuddle.com/svn/hoge',       #アカウント:SVN
	},
	:backup_settings => {
		:rootpath       => 'C:',                                  #バックアップディレクトリを作成するディレクトリ
		:backupdir      => 'svnbackup'                            #出力するバックアップディレクトリ
	}
}

# インスタンスを生成
exe =  UnfuddleApiWrpp.new(settings)

# Unfuddleへ最終リビジョンを要求し
response = exe.query(:changesets, :lastest)
# 結果をXMLで取得し
lastestRev = exe.getXMLElement(response, "changeset/revision")
# SVNリポジトリのバックアップを行う
result = exe.svnbackup(lastestRev.to_i)

そうすると、指定したディレクトリにSVNリポジトリのバックアップが作成されます。
Revが1しかあがっていなくても全てのREVを取得します・・・
差分機能は未実装です。

APIラッパーのみの使い方

require 'unfuddleapiwrpp'

# --------------------------------------------------------------
# 【パラメータ】
# 1.Unfuddleに関する設定
# --------------------------------------------------------------
# Unfuddleに関する情報
settings = {
	:unfuddle_settings => {
		:subdomain  => 'hoge',                                    #アカウント:サブドメイン
		:username   => 'hoge',                                    #アカウント:ユーザー名
		:password   => 'passwordxx',                              #アカウント:パスワード
		:url        => 'http://hoge.unfuddle.com/svn/hoge',       #アカウント:SVN
	}
}

# インスタンスを生成
exe =  UnfuddleApiWrpp.new(settings)

# Unfuddleへアカウント情報を要求し((3つ目のパラメータには明示的にXMLを渡してもOK))
response = exe.query(:projects, :projects, 'xml')
# XML形式からプロジェクトIDを取得する
projectID = exe.getXMLElement(response,"projects/project/id")

APIラッパーでJSONを使う

実はこちらの方が便利な気がする。
デフォルトはこちらにすべき??

require 'unfuddleapiwrpp'

# --------------------------------------------------------------
# 【パラメータ】
# 1.Unfuddleに関する設定
# --------------------------------------------------------------
# Unfuddleに関する情報
settings = {
	:unfuddle_settings => {
		:subdomain  => 'hoge',                                    #アカウント:サブドメイン
		:username   => 'hoge',                                    #アカウント:ユーザー名
		:password   => 'passwordxx',                              #アカウント:パスワード
		:url        => 'http://hoge.unfuddle.com/svn/hoge',       #アカウント:SVN
	}
}

# インスタンスを生成
exe =  UnfuddleApiWrpp.new(settings)

# JSONでリビジョン取得する場合
response = exe.query(:changesets, :lastest, 'json')
lastestRev = JSON.parse(response.body)["revision"]

# JSONでアカウントのメッセージ取得する場合
response2 = exe.query(:accounts, :account, 'json')
message = JSON.parse(response.body)["message"]

実装したAPI一覧

あとで書く予定。

ソースコード

UnfuddleApiWrpp.rb

# = UnfuddleAPI Wrapper & SVNBackUP
#
# Author::  Guyon
# Version:: 0.1
#
# Unfuddleで提供されているAPIから任意の形式で値を取得し、
# SVNリポジトリのバックアップを行います。
#
# このクラスを使用するには「json」と「tmail」のインスト
# ールが必要です。
#
# 主な機能:
#
# 1.任意の形式でUnfuddleからデータを取得します。*1 *2
# 2.前回行ったリポジトリバックアップよりRevが新しければ
#  SVNリポジトリのバックアップを行う
# 3.ログ、お知らせメール
#
# ※ *1 Version 0.1での取得形式は「XML」と「JSON」のみ対応
#   *2 Version 0.1ではAccount、project、changesetのみ対応

require 'rubygems'
require 'logger'
require 'kconv'
require 'net/https'
require 'net/smtp'
require 'rexml/document'
require 'tmail'
require 'json'
include REXML

# ===================================================
# Unfuddleのリポジトリをバックアップ
# ===================================================
class UnfuddleApiWrpp

	# バックアップディレクトリを日付で作成
	@@makedir  = Time.now.strftime("%Y%m%d%H%M%S")
	# UnfuddleAPIのアドレス
	@@unfuddle_urls = {
		:accounts => {
			:account               => {:uri => 'account'                                                  , :method => 'PUT'},
			:activity              => {:uri => 'account/activity'                                         , :method => 'GET'},
			:search                => {:uri => 'account/search'                                           , :method => 'GET'},
			:resetAccessKeys       => {:uri => 'account/reset_access_keys'                                , :method => 'PUT'}
		},
		:projects => {
			:projects              => {:uri => 'projects'                                                 , :method => 'GET'},
			:archives              => {:uri => 'archives'                                                 , :method => 'GET'},
			:components            => {:uri => 'components'                                               , :method => 'GET'},
			:severities            => {:uri => 'severities'                                               , :method => 'GET'},
			:versions              => {:uri => 'versions'                                                 , :method => 'GET'},
			:project               => {:uri => 'projects/${projectId}'                                    , :method => 'GET'},
			:activity              => {:uri => 'projects/${projectId}/activity'                           , :method => 'GET'},
			:search                => {:uri => 'projects/${projectId}/search'                             , :method => 'GET'},
			:dump                  => {:uri => 'projects/${projectId}/dump'                               , :method => 'PUT'},
			:archive               => {:uri => 'archives/${projectId}/archives'                           , :method => 'GET'},
			:archive_download      => {:uri => 'archives/${projectId}/archives/download'                  , :method => 'GET'},
			:component             => {:uri => 'components/${projectId}'                                  , :method => 'GET'},
			:severitie             => {:uri => 'severities/${projectId}'                                  , :method => 'GET'},
			:version               => {:uri => 'versions/${projectId}'                                    , :method => 'GET'}
		},
		:changesets => {
			:changeset             => {:uri => 'projects/${projectId}/changesets'                         , :method => 'POST'},
			:lastest               => {:uri => 'projects/${projectId}/changesets/latest'                  , :method => 'GET'},
			:processMessageActions => {:uri => 'projects/${projectId}/changesets/process_message_actions' , :method => 'GET'}
		}
	}

	# コンストラクタ Hashで初期設定値を受け取る
	def initialize(settings = nil)
		unless settings
			return
		end

		@mail_settings     = settings.has_key?(:mail_settings)     ? settings[:mail_settings] : nil
		@unfuddle_settings = settings.has_key?(:unfuddle_settings) ? settings[:unfuddle_settings] : nil
		@backup_settings   = settings.has_key?(:backup_settings)   ? settings[:backup_settings] : nil
		@options           = settings.has_key?(:options)           ? settings[:backup_settings] : nil

		# バックアップ
		if @backup_settings
			#対象ディレクトリ
			@backup_fullpath   = @backup_settings[:rootpath] + '\\' + @backup_settings[:backupdir] + '\\' + @@makedir
			# リビジョン管理ファイルパス
			@lastestSyncRevLogFile = @backup_settings[:rootpath] + '\\' + @backup_settings[:backupdir] + '\\' + 'lastest_sync_rev_log.txt'          #リビジョンログを残すファイル名
		end

		# ロガー
		@use_log = @options[:use_log] if @options && @options.has_key?(:use_log)
		if @use_log
			@log = Logger.new(@options[:logPath])
			#ログレベル
			case @options[:logLevel]
			when 1
				@log.level = Logger::DEBUG
			when 2
				@log.level = Logger::INFO
			when 3
				@log.level = Logger::WARN
			when 4
				@log.level = Logger::ERROR
			when 5
				@log.level = Logger::FATAL
			else
				@log.level = Logger::FATAL
			end
		end

		# メール
		@use_mail = @options[:use_mail] if @options && @options.has_key?(:use_mail)

	end

	# レスポンスフォーマットを指定して問い合わせ実行
	def query(menuname, action, format = 'xml')
		# TODO 本当は再帰的に問い合わせをしてパラメータをよしなに解決したい
		#      現在はprojectIdのみが必要なので、固定として処理する

		# プロジェクトIDが必要なリクエストなら、projectIdを取得する
		params = Hash.new
		if @@unfuddle_urls[menuname][action][:uri] =~ /\$\{projectId\}/
			# パラメータリストにプロジェクトIDをセットする
			params[:projectId] = query_projects
		end
		# resoponsを返す
		return getRespons(@@unfuddle_urls[menuname][action], format, params)
	end

	# プロジェクトIDを取得する
	def query_projects
		response = getRespons(@@unfuddle_urls[:projects][:projects] ,'xml' , {})
		# XMLオブジェクトを作成
		return getXMLElement(response,"projects/project/id").to_i
	end

	# responsで取得したXMLから要素を取得する
	def getXMLElement(xmlRespons,filter)
		doc = REXML::Document.new(xmlRespons.body)
		REXML::XPath.first(doc,filter).text
	end

	#--------------------------------------------
	# GET通信を行い結果を取得する
	#--------------------------------------------
	def getRespons(target, format, params)
		http = Net::HTTP.new("#{@unfuddle_settings[:subdomain]}.unfuddle.com", @unfuddle_settings[:ssl] ? 443 : 80)

		# SSLを使用するならHTTPクライアントに設定しておく
		if @unfuddle_settings[:url] =~ /^https/
			http.use_ssl = true
			http.verify_mode = OpenSSL::SSL::VERIFY_NONE
		end

		begin
			# uriのテンプレートを取得し
			uri = target[:uri]
			# パラメータが存在すればテンプレートuriを置換する
			params.each_pair{|key, value|
				uri = uri.sub(/\$\{#{key}\}/, value.to_s)
			}
			# リクエスト情報をHTTPクライアントに設定する
			request = Net::HTTP::Get.new('/api/v1/' + uri + '.' + format)
			request.basic_auth @unfuddle_settings[:username], @unfuddle_settings[:password]

			# HTTP通信を行い結果を取得し
			response = http.request(request)

			# レスポンスコードによりエラーを判断する
			unless response.code == "200"
				# レスポンス結果が失敗だったらエラーコードを出力する
				puts "HTTP Status Code: #{response.code}."
				@log.error("HTTP Status Code: #{response.code}.") if @use_log
				sendmail_by_tmail("HTTP Status Code: #{response.code}.") if @use_mail
			end
		rescue => e
			# 障害ログ
			@log.fatal(e) if @use_log
			sendmail_by_tmail(e) if @use_mail
			return nil
		end

		return response
	end

	# ------------------------------------------------------------
	# メール送信処理
	#
	# action_mailer > TMail > 標準ライブラリの順で楽に実装できる?
	# ここはあえて、[action_mailer]ではなく[TMail]を選択
	# @see http://code.nanigac.com/source/view/339
	# ------------------------------------------------------------
	def sendmail_by_tmail(body)
		# Mailオブジェクトを生成する
		mail = TMail::Mail.new

		# 送信情報を設定する
		mail.to       = @mail_settings[:to]
		mail.from     = @mail_settings[:from]
		mail.reply_to = @mail_settings[:from]

		# メールを日本語に対応させる
		work = Kconv.tojis(@mail_settings[:subject]).split(//,1).pack('m').chomp
		mail.subject = "=?ISO-2022-JP?B?"+work.gsub('\n', '')+"?="
		mail.body = Kconv.tojis(body)

		# その他
		mail.date = Time.now
		mail.mime_version = '1.0'
		mail.set_content_type 'text', 'plain', {'charset'=>'iso-2022-jp'}

		# マルチパートに対応する為に上書き?
		mail.write_back

		# 送信する
		Net::SMTP.start(@mail_settings[:smtp]){ |smtp|
			smtp.sendmail(mail.encoded, mail.from, mail.to)
		}
	end

	#SVNのバックアップを取る
	def svnbackup(lastestRev)
		# バックアップのパラメータ設定がされていなければ処理を行わない
		return unless @backup_fullpath

		# 前回同期した時のRevisonの値を読み込む
		lastestSyncRev = nil
		begin
			File::open(@lastestSyncRevLogFile, "r"){|f| lastestSyncRev = f.read.chomp.to_i}
		rescue => e
			# ファイルがなかった時は警告ログ出力しておき
			@log.fatal(e) if @use_log
			# メールも出しておく
			sendmail_by_tmail(e) if @use_mail
		end

		# 前回同期時のリビジョンが不明か新しくなっていたらバックアップを処理を行う
		if lastestSyncRev.blank? || lastestRev > lastestSyncRev

			# リポジトリを作成する
			exit unless runSvnCmd("svnadmin create " + @backup_fullpath)

			# フックする為にpre-revprop-changeの設定値を行う
			if ENV["OS"] =~ /Windows/
				# Windowsならフックの為のpre-revprop-change.batを作成する。
				File::open(@backup_fullpath + "\\hooks\\pre-revprop-change.bat", "w"){|f|
				f.puts "exit 0"
			}
			else
				# Windows以外はUnix系と想定しているのでShellを編集する
				File::open(@backup_fullpath + "\\hooks\\pre-revprop-change.tmpl", "w"){|f|
					f.puts "#!bin/sh"
					f.puts "exit 0"
				}
			end

			# リポジトリを初期化する
			exit unless runSvnCmd("svnsync init file:///" + @backup_settings[:backupdir] + "/" + @@makedir + " " + @unfuddle_settings[:url])

			# リポジトリを同期化する
			exit unless runSvnCmd("svnsync sync file:///" + @backup_settings[:backupdir] + "/" + @@makedir)
		end

		# 最後にログとして最後に同期を行ったリビジョンを書き込む
		File::open(@lastestSyncRevLogFile, "w"){|f|
			f.puts lastestRev
		}
	end

	# SVNコマンド処理を行う
	# 正常時:true 異常持:false
	def runSvnCmd(cmdStr)
		isSvnCmd = system(cmdStr)
		# 実行エラーの場合はログ出力
		unless isSvnCmd
			@log.fatal("SVNディレクトリのバックアップ処理に失敗しました。(" + cmdStr + ")")
			sendmail_by_tmail(e) if @use_mail
			return false
		end
		return true
	end
end

TODO

*1:SVNのバックアップ部分が主に

*2:実行しても処理は行われずnilを返します