*バックアップデータ構造の改善 [#pc299b0e] -ページ: [[BugTrack2]] -投稿者: [[Cue]] -優先順位: 低 -状態: 提案 -カテゴリー: 本体新機能 -投稿日: 2005-08-31 (水) 03:50:07 -バージョン: **メッセージ [#v43ed818] **従来の仕様と特徴 (- PukiWiki 1.4.6) [#m5a5c2e9] ***バックアップファイル構造 [#i562cea6] +バックアップファイル1ファイルの中にそのページの全ての世代が保存されている ++ディスクスペースは少なくて良い(特に小さいページが沢山ある場合) +Boundaryは、PKWK_SPLITTER\sUTIME\n +データ中にBoundaryが現れた場合、行末に半角スペースを加える ++保存データに手を加えるので処理としては良くない ++PKWK_SPLITTERは過去にはglobal $splitter ++\sに半角スペース以外が使われた事はない +zlib extensionの有無でファイル形式が変わる((PHP 4.3.0 以降、zlib モジュールは php バイナリにビルトインされたため、PHP4.2.x以前+zlibなし→PHP4.3.0以降でのみ発生する)) ++設置サーバーの状況が変わると手作業しか移行手段がない(zlib無で.gz対応は不可能) ++Boundaryも含めて圧縮されるので、一部の損傷が全滅につながる ***backup.php内で外部からコールされる関数 [#ybb94d95] -make_backup バックアップ追加処理 -get_backup バックアップ全データの取得(過去には一覧取得と各データ取得で分けられていたようだが今は全取得しか使われていない) -_backup_file_exists バックアップの有無確認 -_backup_delete バックアップの全削除 -直接データ構造にアクセスする個所 --dump.inc.php、backup.inc.php(バックアップの存在する全ページ名の取得) ***backup.inc.phpとの連携部など [#sad248ef] +常に解凍された全データを要求する ++複数回のファイル読み込みと解凍を要さない ++バックアップファイルが大きくなると処理の負荷が大きい(BugTrack/732) +各バックアップは世代番号で管理される。世代番号はバックアップファイル内に存在する順で割り当てられる ++一覧表示から各データの取得までにバックアップ処理が起こると世代番号がずれる +バックアップ日時は、そのページの前回の最終更新日時(そのデータが作られた日時)が割り当てられる ++タイムスタンプを変更しない編集を繰り返すと同じタイムスタンプのバックアップが複数作られる(BugTrack/685) +$cycle=0 でも同一タイムスタンプでバックアップは働かない ++重いバックアップ処理を連打されずにすむ ++UTIMEでtouchはしていないので処理が重い場合など若干のずれが生じる #comment ** 問題点 [#p5cf8c63] *** backupに対する既存の機能要望 [#xda38abf] +自動生成されるページを含めて基本は全ページが対象(BugTrack/708) +世代を指定してバックアップを削除できない([[org:欲しいプラグイン/137]]) +通常バックアップと別に版管理を行いたい(BugTrack2/86) --バックアップ時刻の外に一覧用の別データを埋め込める構造にする?要望した人は別の方法を見付けたみたいですが。 + backupじゃなくて(あるいは選択式で)データベースが使えるようになって欲しい --backup.phpをカプセル化して標準データとの相互変換コードを用意すれば内部構造をどう変えても対応可能 #comment **改善案 [#j62ef48c] ***1ファイルは維持し、日付部分は圧縮しない案 [#f7b3dda2] +ファイル読み込み・一覧作成の段階で解凍は発生しないので比較的低負荷 +一部が損傷しても全体に影響は及ばない +ディスクスペースは若干増える +圧縮部分に偶然Boundaryが現れる可能性にどう対処するか ++普通は各ブロックサイズを記録しておいてfseekしながら飛ばし読みする ***各バックアップ世代をページ名のディレクトリ下に独立ファイル化する案 [#ybc4136a] +一覧作成はディレクトリ検索速度に依存する(必要メモリは少ないが、システムコールは多いはず) +一部が損傷しても全体に影響は及ばない +ディスクスペースは増える(特にFATファイルシステムで) +Boundaryとバックアップ読み込み時のflock処理が不要 +特定の世代だけ削除するのが容易(ftpでも可能) +dump.inc.phpが1ファイル状態を前提にファイルにアクセスしている ++データ構造に直接アクセスすべきではない。必要ならAPIを作る。 ***CVS likeに差分のみで全体を構成する案 [#a33815f5] +無圧縮でも非常に小さいサイズになる ++昨今の状況ではディスクスペースをそれほど重視しなくて良いのでは? ***世代管理にバックアップ時刻を使う案 [#s2f006dc] +ページのタイムスタンプを変更しなくても編集した時刻が分かってしまう ++Recentには影響しないので構わないのでは? +ページの最終更新日時はlast-modified headerにも使われるので、タイムスタンプを更新しない場合でも変更した方が良いはず。Recentに入れない等の処理は別の方法で実現した方が良いのでは? +暫定的に差分の最終更新日時から得る方法もあり ***backup.phpに必要とされるAPI [#e61473b0] +バックアップの追加 (現在のmake_backup) +バックアップの有無 (現在の_backup_file_exists) +バックアップデータ一覧の取得 +ある世代のバックアップデータの取得 +バックアップの全削除 (現在の_backup_delete) +バックアップの部分削除 +バックアップの存在する全ページ名の一覧取得 +バックアップデータを標準データと相互変換する機能 -get_backupでの全取得はやめる -どの形式を標準データにするか? --互換性で問題がないのは現在の構造で無圧縮 -dump.inc.phpは標準データを扱うようにする -バックアップデータの変換はタイムアウト対策が必須 -バックアップデータを一行ずつの配列(file()で読み込んだソース相当)で渡す必要はないのでは? --(array)$multi_line_strの形で返しても問題無いみたいです ---- -過去(現状の調査と把握)を踏まえて進めようとされていて、ここにわかり易く解説していらっしゃる、それだけでも素晴らしい仕事だと思います ;) -- [[henoheno]] &new{2005-09-01 (木) 23:10:03}; -他に気付いた点や案があれば追加お願いします。 -- [[Cue]] &new{2005-09-03 (土) 00:39:25}; #comment **実装案 [#k0c7545d] 弄ってみました。長くてすいません。((どこかのアップローダーに置いた方が良いかなぁ…)) ***実装していて気付いた点など [#jb8ea4a2] +$maxageは正の整数を期待しているがノーチェック --暫定的に0以下でバックアップ無しの動作に(世代無制限の方が正しいかもしれない) +バックアップリストの配列が1からスタート --backup.inc.php側で処理すれば0オリジンで構わないので変更してしまいました +標準データ形式をどこまでサポートするか --標準データ形式はコンバート用の一時ファイルになっています(ネイティブ形式以外は一覧が見られない等々) +データコンバート処理 --コンバート中にタイムアウトが発生した場合も不完全ながらレジューム可能 ---処理経過をコンバートするファイルの有無に頼っている ---変換前と後のファイルが同時に存在する場合は途中で中断した場所とみなしてNoticeを出してその処理を飛ばす --メモリが少ない状況でもコンバート処理だけは可能な限り動作させる --既存データに同一タイムスタンプがあった場合、1秒加算させている ---今はディレクトリ管理のみ。既存の形式でもコンバートしないと''一覧で選択できない''(対応急務) +必要メモリ --既存のバックアップ形式については速度優先で全データを読み込んでstatic変数に貯めて処理している ---省メモリにも変えられるが、ファイルを2回スキャンしなければならない +Noticeの出力 --die_messageで処理を中断するほどではないが、データ不整合等を検出した場合に何らかの警告を出す統一した処理が欲しい ---とりあえずerror_log($msg, 0)で出力してみる(これが正しいかは不明) ***未実装な点 [#dda2946a] -backup.inc.phpで特定の世代だけ削除する機能が未サポート --どなたかUI作るの得意な方よろしくお願いします(backup_delete($page, $utime)で機能は呼べます) ---- #comment **差分と差し替えファイル [#vae03bc7] --- /lib/file.php Sun Jul 03 23:16:24 2005 +++ /lib/file.php Mon Sep 12 00:26:52 2005 @@ -36,14 +36,14 @@ $postdata = make_str_rules($postdata); + // Create backup + make_backup($page, $postdata == ''); // Is $postdata null? + // Create and write diff $oldpostdata = is_page($page) ? join('', get_source($page)) : ''; $diffdata = do_diff($oldpostdata, $postdata); file_write(DIFF_DIR, $page, $diffdata); - // Create backup - make_backup($page, $postdata == ''); // Is $postdata null? - // Create wiki text file_write(DATA_DIR, $page, $postdata, $notimestamp); --- /plugin/backup.inc.php Sun May 08 01:21:52 2005 +++ /plugin/backup.inc.php Wed Sep 07 22:11:32 2005 @@ -9,60 +9,72 @@ function plugin_backup_action() { - global $vars, $do_backup, $hr; - global $_msg_backuplist, $_msg_diff, $_msg_nowdiff, $_msg_source, $_msg_backup; - global $_msg_view, $_msg_goto, $_msg_deleted; - global $_title_backupdiff, $_title_backupnowdiff, $_title_backupsource; - global $_title_backup, $_title_pagebackuplist, $_title_backuplist; + global $vars, $do_backup; + global $_title_pagebackuplist, $_title_backuplist; if (! $do_backup) return; $page = isset($vars['page']) ? $vars['page'] : ''; + $action = isset($vars['action']) ? $vars['action'] : ''; + + if (preg_match('/^(?:im|ex)port$/', $action)) return plugin_backup_convert($action); if ($page == '') return array('msg'=>$_title_backuplist, 'body'=>plugin_backup_get_list_all()); check_readable($page, true, true); - $s_page = htmlspecialchars($page); - $r_page = rawurlencode($page); - $action = isset($vars['action']) ? $vars['action'] : ''; if ($action == 'delete') return plugin_backup_delete($page); - $s_action = $r_action = ''; - if ($action != '') { - $s_action = htmlspecialchars($action); - $r_action = rawurlencode($action); - } + if (isset($vars['age']) && is_numeric($vars['age']) && + ($vars['age'] += 0) > 0 && is_int($vars['age'])) + return plugin_backup_get_detail($vars['age'], $page, $action); + + return array( 'msg'=>$_title_pagebackuplist, 'body'=>plugin_backup_get_list($page)); +} + +function plugin_backup_get_detail($s_utime, $page, $action) +{ + global $hr, $_msg_view, $_msg_goto, $_msg_deleted, $_msg_nobackup; + global $_msg_backuplist, $_msg_diff, $_msg_nowdiff, $_msg_source, $_msg_backup; + global $_title_backupdiff, $_title_backupnowdiff, $_title_backupsource, $_title_backup; + + $s_page = htmlspecialchars($page); + $r_page = rawurlencode($page); - $s_age = (isset($vars['age']) && is_numeric($vars['age'])) ? $vars['age'] : 0; - if ($s_age == 0) return array( 'msg'=>$_title_pagebackuplist, 'body'=>plugin_backup_get_list($page)); + $s_action = htmlspecialchars($action); + $r_action = rawurlencode($action); $script = get_script_uri(); $body = '<ul>' . "\n"; $body .= ' <li><a href="' . $script . '?cmd=backup">' . $_msg_backuplist . '</a></li>' ."\n"; - $href = $script . '?cmd=backup&page=' . $r_page . '&age=' . $s_age; + $href = $script . '?cmd=backup&page=' . $r_page . '&age=' . $s_utime; $is_page = is_page($page); - if ($is_page && $action != 'diff') - $body .= ' <li>' . str_replace('$1', '<a href="' . $href . - '&action=diff">' . $_msg_diff . '</a>', - $_msg_view) . '</li>' . "\n"; - - if ($is_page && $action != 'nowdiff') - $body .= ' <li>' . str_replace('$1', '<a href="' . $href . - '&action=nowdiff">' . $_msg_nowdiff . '</a>', - $_msg_view) . '</li>' . "\n"; - - if ($action != 'source') - $body .= ' <li>' . str_replace('$1', '<a href="' . $href . - '&action=source">' . $_msg_source . '</a>', - $_msg_view) . '</li>' . "\n"; - - if ($action) - $body .= ' <li>' . str_replace('$1', '<a href="' . $href . - '">' . $_msg_backup . '</a>', - $_msg_view) . '</li>' . "\n"; + $backups = get_backup($page); + $s_age = array_search($s_utime, $backups); + + if (is_int($s_age)) { + if ($is_page && $action != 'diff') + $body .= ' <li>' . str_replace('$1', '<a href="' . $href . + '&action=diff">' . $_msg_diff . '</a>', + $_msg_view) . '</li>' . "\n"; + + if ($is_page && $action != 'nowdiff') + $body .= ' <li>' . str_replace('$1', '<a href="' . $href . + '&action=nowdiff">' . $_msg_nowdiff . '</a>', + $_msg_view) . '</li>' . "\n"; + + if ($action != 'source') + $body .= ' <li>' . str_replace('$1', '<a href="' . $href . + '&action=source">' . $_msg_source . '</a>', + $_msg_view) . '</li>' . "\n"; + + if ($action) + $body .= ' <li>' . str_replace('$1', '<a href="' . $href . + '">' . $_msg_backup . '</a>', + $_msg_view) . '</li>' . "\n"; + } if ($is_page) { $body .= ' <li>' . str_replace('$1', @@ -72,35 +84,39 @@ $body .= ' <li>' . str_replace('$1', $s_page, $_msg_deleted) . "\n"; } - $backups = get_backup($page); if (! empty($backups)) { - $body .= ' <ul>' . "\n"; - foreach($backups as $age => $val) { - $date = format_date($val['time'], TRUE); - $body .= ($age == $s_age) ? - ' <li><em>' . $age . ' ' . $date . '</em></li>' . "\n" : - ' <li><a href="' . $script . '?cmd=backup&action=' . - $r_action . '&page=' . $r_page . '&age=' . $age . - '">' . $age . ' ' . $date . '</a></li>' . "\n"; + $body .= ' <ol>' . "\n"; + foreach($backups as $age=>$utime) { + $date = format_date($utime, TRUE); + if ($age == $s_age) { + $body .= ' <li><em>' . $date . '</em></li>' . "\n"; + } else { + $body .= ' <li><a href="' . $script . '?cmd=backup&action=' . + $r_action . '&page=' . $r_page . '&age=' . $utime . + '">' . $date . '</a></li>' . "\n"; + } } - $body .= ' </ul>' . "\n"; + $body .= ' </ol>' . "\n"; } $body .= ' </li>' . "\n"; $body .= '</ul>' . "\n"; - if ($action == 'diff') { + if (!is_int($s_age)) { + $title = str_replace('(No.$2)', '', $_title_backup); + $body .= str_replace('$1', format_date($s_utime, TRUE), $_msg_nobackup); + } else if ($action == 'diff') { $title = & $_title_backupdiff; - $old = ($s_age > 1) ? join('', $backups[$s_age - 1]['data']) : ''; - $cur = join('', $backups[$s_age]['data']); + $old = $s_age > 0 ? implode('', get_backup($page, $backups[$s_age-1])) : ''; + $cur = implode('', get_backup($page, $s_utime)); $body .= plugin_backup_diff(do_diff($old, $cur)); - } else if ($s_action == 'nowdiff') { + } else if ($action == 'nowdiff') { $title = & $_title_backupnowdiff; - $old = join('', $backups[$s_age]['data']); - $cur = join('', get_source($page)); + $old = implode('', get_backup($page, $s_utime)); + $cur = implode('', get_source($page)); $body .= plugin_backup_diff(do_diff($old, $cur)); - } else if ($s_action == 'source') { + } else if ($action == 'source') { $title = & $_title_backupsource; - $body .= '<pre>' . htmlspecialchars(join('', $backups[$s_age]['data'])) . + $body .= '<pre>' . htmlspecialchars(implode('', get_backup($page, $s_utime))) . '</pre>' . "\n"; } else { if (PLUGIN_BACKUP_DISABLE_BACKUP_RENDERING) { @@ -108,11 +124,11 @@ } else { $title = & $_title_backup; $body .= $hr . "\n" . - drop_submit(convert_html($backups[$s_age]['data'])); + drop_submit(convert_html(implode('', get_backup($page, $s_utime)))); } } - return array('msg'=>str_replace('$2', $s_age, $title), 'body'=>$body); + return array('msg'=>str_replace('$2', $s_age+1, $title), 'body'=>$body); } // Delete backup @@ -121,17 +137,17 @@ global $vars, $_title_backup_delete, $_title_pagebackuplist, $_msg_backup_deleted; global $_msg_backup_adminpass, $_btn_delete, $_msg_invalidpass; - if (! _backup_file_exists($page)) + if (! backup_file_exists($page)) return array('msg'=>$_title_pagebackuplist, 'body'=>plugin_backup_get_list($page)); // Say "is not found" $body = ''; if (isset($vars['pass'])) { if (pkwk_login($vars['pass'])) { - _backup_delete($page); - return array( - 'msg' => $_title_backup_delete, - 'body' => str_replace('$1', make_pagelink($page), $_msg_backup_deleted) - ); + if(backup_delete($page)) + $body = str_replace('$1', make_pagelink($page), $_msg_backup_deleted); + else + $body = '削除失敗'; + return array('msg' => $_title_backup_delete, 'body' => $body); } else { $body = '<p><strong>' . $_msg_invalidpass . '</strong></p>' . "\n"; } @@ -161,7 +177,7 @@ $str = htmlspecialchars($str); $str = preg_replace('/^(\-)(.*)$/m', '<span class="diff_removed"> $2</span>', $str); $str = preg_replace('/^(\+)(.*)$/m', '<span class="diff_added"> $2</span>', $str); - $str = trim($str); + $str = rtrim($str); $str = <<<EOD $hr <ul> @@ -189,13 +205,15 @@ <ul> EOD; $retval[1] = "\n"; - $retval[2] = <<<EOD - </ul> + $retval[2] = " </ul>\n <ol>\n"; + $retval[3] = ''; + $retval[4] = <<<EOD + </ol> </li> </ul> EOD; - $backups = _backup_file_exists($page) ? get_backup($page) : array(); + $backups = get_backup($page); if (empty($backups)) { $msg = str_replace('$1', make_pagelink($page), $_msg_nobackup); $retval[1] .= ' <li>' . $msg . '</li>' . "\n"; @@ -209,17 +227,17 @@ $href = $script . '?cmd=backup&page=' . $r_page . '&age='; $_anchor_from = $_anchor_to = ''; - foreach ($backups as $age=>$data) { + foreach ($backups as $utime) { if (! PLUGIN_BACKUP_DISABLE_BACKUP_RENDERING) { - $_anchor_from = '<a href="' . $href . $age . '">'; + $_anchor_from = '<a href="' . $href . $utime . '">'; $_anchor_to = '</a>'; } - $date = format_date($data['time'], TRUE); - $retval[1] .= <<<EOD - <li>$_anchor_from$age $date$_anchor_to - [ <a href="$href$age&action=diff">$_msg_diff</a> - | <a href="$href$age&action=nowdiff">$_msg_nowdiff</a> - | <a href="$href$age&action=source">$_msg_source</a> + $date = format_date((int)$utime, TRUE); + $retval[3] .= <<<EOD + <li>$_anchor_from$date$_anchor_to + [ <a href="$href$utime&action=diff">$_msg_diff</a> + | <a href="$href$utime&action=nowdiff">$_msg_nowdiff</a> + | <a href="$href$utime&action=source">$_msg_source</a> ] </li> EOD; @@ -233,12 +251,57 @@ { global $cantedit; - $pages = array_diff(get_existpages(BACKUP_DIR, BACKUP_EXT), $cantedit); + $pages = array_diff(backup_file_exists(), $cantedit); if (empty($pages)) { return ''; } else { return page_list($pages, 'backup', $withfilename); } +} + +function plugin_backup_convert($action) +{ + global $vars, $_msg_invalidpass; + + if(!function_exists('backup_import') || !function_exists('backup_export')) + die_message('This lib/backup.php is standard data format'); + + $title_backup_convert = 'バックアップ形式の変更'; + $msg_import = '標準バックアップ形式からインポートする'; + $msg_export = '標準バックアップ形式にエクスポートする'; + $msg_adminpass = '管理者パスワード'; + $msg_complete = '変更完了'; + $btn_submit = '実行'; + $body = ''; + if (isset($vars['pass'])) { + if (pkwk_login($vars['pass'])) { + if($action == 'import'){ + backup_import(FALSE); + return array('msg' => $title_backup_convert, 'body' => $msg_complete); + } + backup_export(FALSE); + return array('msg' => $title_backup_convert, 'body' => $msg_complete); + } else { + $body = '<p><strong>' . $_msg_invalidpass . '</strong></p>' . "\n"; + } + } + + $script = get_script_uri(); + $body .= <<<EOD +<form action="$script" method="post"> + <div> + <input type="hidden" name="cmd" value="backup" /> + <input type="radio" name="action" value="import" id="_p_backup_import" checked="checked" /> + <label for="_p_backup_import">$msg_import</label><br /> + <input type="radio" name="action" value="export" id="_p_backup_export" /> + <label for="_p_backup_export">$msg_export</label><br /> + <label for="_p_backup_adminpass">$msg_adminpass</label> + <input type="password" name="pass" size="12" id="_p_backup_adminpass" /> + <input type="submit" name="ok" value="$btn_submit" /> + </div> +</form> +EOD; + return array('msg'=>$title_backup_convert, 'body'=>$body); } ?> backup.phpは差分の方が長くなるので… <?php /** * * PukiWiki - Yet another WikiWikiWeb clone. * * backup.php * * バックアップを管理する * * @package org.pukiwiki * @access public * @author * @create * @version $Id: backup.php,v 1.9 2005/04/30 05:21:00 henoheno Exp $ * Copyright (C) * 2002-2005 PukiWiki Developers Team * 2001-2002 Originally written by yu-ji * License: GPL v2 or (at your option) any later version **/ if(extension_loaded('zlib')){ define('BACKUP_EXT', '.gz'); if(version_compare(PHP_VERSION, '4.3.0') < 0) define('BACKUP_PROTOCOL_WRAPPER', 'zlib:'); else define('BACKUP_PROTOCOL_WRAPPER', 'compress.zlib://'); }else{ define('BACKUP_EXT', '.txt'); define('BACKUP_PROTOCOL_WRAPPER', ''); } /** * make_backup * バックアップを作成する * * @access public * @param String $page ページ名 * @param Boolean $delete TRUE:バックアップを削除する * * @return Void */ function make_backup($page, $delete = FALSE) { global $cycle, $maxage; global $do_backup, $del_backup; if (PKWK_READONLY || ! $do_backup) return; if (($del_backup && $delete) || $maxage <= 0) { backup_delete($page); return; } if (! is_page($page)) return; $lastmod = _backup_get_filetime($page); if ($lastmod == 0 || UTIME - $lastmod > 60 * 60 * $cycle) { $regex_splitter = '/^' . preg_quote(PKWK_SPLITTER, '/') . ' (\d+)\n/m'; $filename = _backup_get_filename($page); $die_msg = 'cannot write file ' . htmlspecialchars($filename) . '<br />maybe permission is not writable or filename is too long'; do{ if(($lock = @fopen($filename, 'r')) !== FALSE){ flock($lock, LOCK_EX); clearstatcache(); if(_backup_get_filetime($page) != $lastmod) break; $fp = @fopen(BACKUP_PROTOCOL_WRAPPER . $filename, 'rb') or die_message('cannot read file ' . htmlspecialchars($filename)); for($data = ''; !feof($fp); $data .= fread($fp, 1048576)); fclose($fp); $backups = preg_split($regex_splitter, $data, -1, PREG_SPLIT_DELIM_CAPTURE); unset($data); array_shift($backups); // 最初のsplitterより前のデータを捨てる(通常は空文字列) $count = (count($backups) >> 1) + 1; // 直後に1件追加するので、(最大件数 - 1)を超える要素を捨てる if ($count > $maxage) array_splice($backups, 0, ($count - $maxage) << 1); }else{ $lock = fopen($filename, 'a+') or die_message($die_msg); flock($lock, LOCK_EX); clearstatcache(); if(filesize($filename) != 0) break; $backups = array(); } // 暫定的に差分から前回の更新日時を得る $utime = filemtime(DIFF_DIR . encode($page) . '.txt') - LOCALZONE; // Escape 'lines equal to PKWK_SPLITTER', by inserting a space $body = PKWK_SPLITTER . ' ' . $utime . "\n"; $body .= rtrim(implode('', preg_replace($regex_splitter, '$1 ', get_source($page)))) . "\n"; $fp = fopen(BACKUP_PROTOCOL_WRAPPER . $filename, 'wb') or die_message($die_msg); while(!empty($backups)) { fputs($fp, PKWK_SPLITTER . ' ' . array_shift($backups) . "\n"); // Splitter format fputs($fp, array_shift($backups)); } fputs($fp, $body); fclose($fp); _backup_set_filetime($page, UTIME); }while(FALSE); flock($lock, LOCK_UN); fclose($lock); } } /** * get_backup * バックアップを取得する * $utime = 0または省略 : 全てのバックアップ作成日時を配列で取得する * $utime > 0 : 指定した世代のバックアップデータを取得する * * @access public * @param String $page ページ名 * @param Integer $utime バックアップの作成日時 * * @return String バックアップ ($utime > 0) * Array バックアップ時刻の配列 ($utime == 0) */ function get_backup($page, $utime = 0) { static $c_page, $data, $lists; if($page != $c_page){ $c_page = $page; $regex_splitter = '/^' . preg_quote(PKWK_SPLITTER, '/') . ' (\d+)\n/m'; $filename = _backup_get_filename($page); if(($lock = @fopen($filename, 'r')) === FALSE) return array(); flock($lock, LOCK_SH); $fp = fopen(BACKUP_PROTOCOL_WRAPPER . $filename, 'rb') or die_message('cannot read file ' . htmlspecialchars($filename)); for($data = ''; !feof($fp); $data .= fread($fp, 1048576)); fclose($fp); flock($lock, LOCK_UN); fclose($lock); $data = preg_split($regex_splitter, $data, -1, PREG_SPLIT_DELIM_CAPTURE); $lists = array(); foreach($data as $key=>$val){ if($key & 1){ unset($data[$key]); $lists[] = (int)$val; } } array_shift($data); } if($utime == 0){ return $lists; } $key = array_search($utime, $lists); return is_int($key) ? (array)$data[$key] : array(); } /** * backup_file_exists * バックアップファイルが存在するか * * @access public * @param String $page ページ名 * * @return Boolean TRUE:ある FALSE:ない * Array バックアップの存在するページの配列(ページ名省略時) */ function backup_file_exists($page = NULL) { if(isset($page)) return file_exists(_backup_get_filename($page)); return get_existpages(BACKUP_DIR, BACKUP_EXT); } /** * backup_delete * バックアップファイルを削除する * * @access public * @param String $page ページ名 * @param Integer $utime バックアップの作成日時 * * @return Boolean FALSE:失敗 */ function backup_delete($page, $utime = NULL) { $filename = _backup_get_filename($page); if(!isset($utime)) return @unlink($filename); if(($lock = @fopen($filename, 'r')) === FALSE) return FALSE; flock($lock, LOCK_EX); do{ $success = FALSE; $regex_splitter = '/^' . preg_quote(PKWK_SPLITTER, '/') . ' (\d+)\n/m'; $filename = _backup_get_filename($page); if(($fp = @fopen(BACKUP_PROTOCOL_WRAPPER . $filename, 'rb')) === FALSE) break; for($data = ''; !feof($fp); $data .= fread($fp, 1048576)); fclose($fp); $backups = preg_split($regex_splitter, $data, -1, PREG_SPLIT_DELIM_CAPTURE); unset($data); array_shift($backups); $lists = array(); foreach($backups as $key=>$val){ if($key & 1 == 0) $lists[$key] = (int)$val; } $key = array_search($utime, $lists); if(!is_int($key)) break; array_splice($backups, $key, 2); if(($fp = @fopen(BACKUP_PROTOCOL_WRAPPER . $filename, 'rb')) === FALSE) break; while(!empty($backups)) { fputs($fp, PKWK_SPLITTER . ' ' . array_shift($backups) . "\n"); // Splitter format fputs($fp, array_shift($backups)); } fclose($fp); $success = _backup_set_filetime($page, $filetime); }while(FALSE); flock($lock, LOCK_UN); fclose($lock); return $success; } /** * backup_import * バックアップ形式を標準データ形式からインポートする * * @access public * @param Boolean $keep_data 元データを削除しない * * @return Void */ function backup_import($keep_data) { if(BACKUP_EXT == '.txt') return; if(ini_get('safe_mode') == '0') set_time_limit(0); foreach(get_existpages(BACKUP_DIR, '.txt') as $page){ $src = BACKUP_DIR . encode($page) . '.txt'; $dist = BACKUP_DIR . encode($page) . BACKUP_EXT; if(file_exists($dist)){ error_log('PukiWiki Notice: ' . htmlspecialchars($src) . ' is skipped.', 0); continue; } $f_src = @fopen($src, 'rb') or die_message('cannot read file ' . htmlspecialchars($src)); $f_dist = @fopen(BACKUP_PROTOCOL_WRAPPER . $dist, 'wb') or die_message('cannot write file ' . htmlspecialchars($dist)); while(!feof($f_src)) fputs($f_dist, fread($f_src, 1048576)); fclose($f_dist); fclose($f_src); touch($dist, filemtime($src)); if(!$keep_data && !@unlink($src)) die_message('cannot remove file ' . htmlspecialchars($src)); } } /** * backup_export * バックアップ形式を標準データ形式にエクスポートする * * @access public * @param Boolean $keep_data 元データを削除しない * * @return Void */ function backup_export($keep_data) { if(BACKUP_EXT == '.txt') return; if(ini_get('safe_mode') == '0') set_time_limit(0); foreach(get_existpages(BACKUP_DIR, BACKUP_EXT) as $page){ $src = BACKUP_DIR . encode($page) . BACKUP_EXT; $dist = BACKUP_DIR . encode($page) . '.txt'; if(file_exists($dist)){ error_log('PukiWiki Notice: ' . htmlspecialchars($src) . ' is skipped.', 0); continue; } $f_src = @fopen(BACKUP_PROTOCOL_WRAPPER . $src, 'rb') or die_message('cannot read file ' . htmlspecialchars($src)); $f_dist = @fopen($dist, 'wb') or die_message('cannot write file ' . htmlspecialchars($dist)); while(!feof($f_src)) fputs($f_dist, fread($f_src, 1048576)); fclose($f_dist); fclose($f_src); touch($dist, filemtime($src)); if(!$keep_data && !@unlink($src)) die_message('cannot remove file ' . htmlspecialchars($src)); } } //////////////// private function follows //////////////// /** * _backup_get_filename * バックアップファイル名を取得する * * @access private * @param String $page ページ名 * * @return String バックアップのファイル名 */ function _backup_get_filename($page) { return BACKUP_DIR . encode($page) . BACKUP_EXT; } /** * _backup_get_filetime * バックアップファイルの更新時刻を得る * * @access private * @param String $page ページ名 * * @return Integer ファイルの更新時刻(GMT) */ function _backup_get_filetime($page) { if (!backup_file_exists($page) || ($time = filemtime(_backup_get_filename($page))) === FALSE) return 0; return $time - LOCALZONE; } /** * _backup_set_filetime * バックアップファイルの更新時刻をセットする * * @access private * @param String $page ページ名 * @param Integer $utime ファイルの更新時刻(GMT) * * @return Boolean FALSE:失敗 */ function _backup_set_filetime($page, $utime) { return touch(_backup_get_filename($page), $utime + LOCALZONE); } ?> ディレクトリで管理するタイプのbackup.php <?php /** * * PukiWiki - Yet another WikiWikiWeb clone. * * backup.php * * バックアップをディレクトリで管理する by Cue * * @package org.pukiwiki * @access public * @author * @create * @version $Id: backup.php,v 1.9 2005/04/30 05:21:00 henoheno Exp $ * Copyright (C) * 2002-2005 PukiWiki Developers Team * 2001-2002 Originally written by yu-ji * License: GPL v2 or (at your option) any later version **/ define('BACKUP_DIR_PERMISSION', 0777); if(extension_loaded('zlib')){ define('BACKUP_EXT', '.gz'); if(version_compare(PHP_VERSION, '4.3.0') < 0) define('BACKUP_PROTOCOL_WRAPPER', 'zlib:'); else define('BACKUP_PROTOCOL_WRAPPER', 'compress.zlib://'); }else{ define('BACKUP_EXT', '.txt'); define('BACKUP_PROTOCOL_WRAPPER', ''); } /** * make_backup * バックアップを作成する * * @access public * @param String $page ページ名 * Boolean $delete TRUE:バックアップを削除する * * @return Void */ function make_backup($page, $delete = FALSE) { global $cycle, $maxage; global $do_backup, $del_backup; if (PKWK_READONLY || ! $do_backup) return; if (($del_backup && $delete) || $maxage <= 0) { backup_delete($page); return; } if (! is_page($page)) return; $backup = &new backup($page); $lastmod = $backup->get_lastmod(); if(UTIME > $lastmod + 60 * 60 * $cycle){ if($lastmod == 0 && !$backup->create_dir()) die_message('cannot create directory ' . htmlspecialchars($backup->dirname) . '<br />maybe permission is not writable or filename is too long'); for($backup_lists = $backup->lists(); count($backup_lists) >= $maxage; $backup->delete(array_shift($backup_lists)) ); // 暫定的に差分から前回の更新日時を得る $utime = filemtime(DIFF_DIR . encode($page) . '.txt') - LOCALZONE; $backup->append(implode('', get_source($page)), $utime); } } /** * get_backup * バックアップを取得する * $utime = 0または省略 : 全てのバックアップ作成時刻を配列で取得する * $utime > 0 : 指定した時刻のバックアップデータを取得する * * @access public * @param String $page ページ名 * Integer $utime バックアップの作成時刻 * * @return String バックアップ ($utime != 0) * Array バックアップ時刻の配列 ($utime == 0) */ function get_backup($page, $utime = 0) { $backup = &new backup($page); if($utime == 0){ return array_keys($backup->lists()); } $lines = $backup->read($utime); return $lines !== FALSE ? $lines : array(); } /** * backup_file_exists * バックアップが存在するか * * @access public * @param String $page ページ名 * * @return Boolean TRUE:ある FALSE:ない * Array バックアップの存在するページ名の配列(ページ名省略時) */ function backup_file_exists($page = NULL) { if(isset($page)){ $backup = &new backup($page); return file_exists($backup->dirname); } return get_existpages(BACKUP_DIR, ''); } /** * backup_delete * バックアップを削除する * * @access public * @param String $page ページ名 * Integer $utime バックアップ作成時刻(省略時は全て) * * @return Boolean FALSE:失敗 */ function backup_delete($page, $utime = NULL) { $backup = &new backup($page); return $backup->delete($utime); } /** * backup_import * バックアップ形式を標準データ形式からインポートする * * @access public * @param Boolean $keep_data 元データを削除しない * * @return Void */ function backup_import($keep_data) { if(ini_get('safe_mode') == '0') set_time_limit(0); $regex_splitter = '/^' . preg_quote(PKWK_SPLITTER, '/') . ' (\d+)\n/m'; foreach(get_existpages(BACKUP_DIR, '.txt') as $page){ $import_file = BACKUP_DIR . encode($page) . '.txt'; $backup = &new backup($page); if(file_exists($backup->dirname)){ error_log('PukiWiki Notice: ' . htmlspecialchars($import_file) . ' is skipped.', 0); continue; } if(!$backup->create_dir()) die_message('cannot create directory ' . htmlspecialchars($backup->dirname) . '<br />maybe permission is not writable or filename is too long'); $fp = @fopen($import_file, 'rb') or die_message('cannot read file ' . htmlspecialchars($import_file)); $buf = $utime = NULL; while(!feof($fp)){ $buf .= fread($fp, 1048576); $data = preg_split($regex_splitter, $buf, -1, PREG_SPLIT_DELIM_CAPTURE); if(!feof($fp)){ $buf = array_pop($data); }else{ unset($buf); } if(isset($utime)){ array_unshift($data, $utime); }else if(!empty($data)){ // 最初のsplitterより前のデータを捨てる(通常は空文字列) array_shift($data); } while(!empty($data)){ $utime = (int)array_shift($data); while(file_exists($backup->filename($utime))) $utime++; // 同一タイプスタンプがある場合、1秒を加算 if(!empty($data)) $backup->append(rtrim(array_shift($data))."\n", $utime); } } fclose($fp); $lists = $backup->lists(); if(empty($lists)) die_message('empty data file found ' . htmlspecialchars($import_file)); $backup->set_lastmod(filemtime($import_file) - LOCALZONE); if(!$keep_data && !@unlink($import_file)) die_message('cannot remove file ' . htmlspecialchars($import_file)); } } /** * backup_export * バックアップ形式を標準データ形式にエクスポートする * * @access public * @param Boolean $keep_data 元データを削除しない * * @return Void */ function backup_export($keep_data) { if(ini_get('safe_mode') == '0') set_time_limit(0); $regex_splitter = '/^(' . preg_quote(PKWK_SPLITTER, '/') . '\s\d+)$/'; foreach(get_existpages(BACKUP_DIR, '') as $page){ $export_file = BACKUP_DIR . encode($page) . '.txt'; $backup = &new backup($page); if(file_exists($export_file)){ error_log('PukiWiki Notice: ' . htmlspecialchars($export_file) . ' is skipped.', 0); continue; } $fp = @fopen($export_file, 'wb') or die_message('cannot write file ' . htmlspecialchars($export_file)); foreach(array_keys($backup->lists()) as $utime){ fputs($fp, PKWK_SPLITTER . ' ' . $utime . "\n"); $lines = $backup->read($utime) or die_message('cannot read file ' . htmlspecialchars($backup->filename($utime))); fputs($fp, implode('', preg_replace($regex_splitter, '$1 ', $lines))); } fclose($fp); touch($export_file, $backup->get_lastmod() + LOCALZONE); if(!$keep_data && !$backup->delete()) die_message('cannot remove directory ' . htmlspecialchars($backup->dirname)); } } //////////////// private function follows //////////////// class backup { var $page; var $dirname; function backup($page){ $this->page = $page; $this->dirname = BACKUP_DIR . encode($this->page) . '/'; } function filename($utime){ return $this->dirname . $utime . BACKUP_EXT; } function get_lastmod(){ $time = @filemtime($this->dirname . 'index.html'); return $time !== FALSE ? $time - LOCALZONE : 0; } function set_lastmod($utime){ return touch($this->dirname . 'index.html', $utime + LOCALZONE); } function create_dir(){ mkdir($this->dirname); chmod($this->dirname, BACKUP_DIR_PERMISSION); if(($fp = @fopen($this->dirname . 'index.html', 'wb')) === FALSE) return FALSE; fputs($fp, "backup files are placed here.\n"); fclose($fp); return TRUE; } function lists(){ $lists = $matches = array(); if(($dp = @opendir($this->dirname)) !== FALSE){ while($filename = readdir($dp)){ if(preg_match('/^(\d+)' . BACKUP_EXT . '$/', $filename, $matches)) $lists[(int)$matches[1]] = $this->dirname . $filename; } closedir($dp); ksort($lists); } return $lists; } function delete($utime = NULL){ if(isset($utime)) return @unlink($this->filename($utime)); $success = TRUE; foreach($this->lists() as $filename) $success &= @unlink($filename); if($success) $success &= @unlink($this->dirname . 'index.html'); return $success ? @rmdir($this->dirname) : FALSE; } function read($utime){ return @file(BACKUP_PROTOCOL_WRAPPER . $this->filename($utime)); } function append($body, $utime){ $filename = $this->filename($utime); $die_msg = 'cannot write file ' . htmlspecialchars($filename) . '<br />maybe permission is not writable or filename is too long'; $lock = @fopen($filename, 'a+') or die_message($die_msg); flock($lock, LOCK_EX); clearstatcache(); if(filesize($filename) === 0){ $fp = @fopen(BACKUP_PROTOCOL_WRAPPER . $filename, 'wb') or die_message($die_msg); fputs($fp, $body); fclose($fp); $this->set_lastmod(UTIME); } flock($lock, LOCK_UN); fclose($lock); } } ?>