diff --git a/admin/inc/class.admin_prefs_sidebox_hooks.inc.php b/admin/inc/class.admin_prefs_sidebox_hooks.inc.php index e65020e4b4..fb51a56d23 100644 --- a/admin/inc/class.admin_prefs_sidebox_hooks.inc.php +++ b/admin/inc/class.admin_prefs_sidebox_hooks.inc.php @@ -1,9 +1,10 @@ + * @author Ralf Becker * @package admin * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ @@ -11,7 +12,6 @@ /** * Static hooks for admin application - * */ class admin_prefs_sidebox_hooks { @@ -21,7 +21,8 @@ class admin_prefs_sidebox_hooks * @var unknown_type */ var $public_functions = array( - 'register_all_hooks' => True + 'register_all_hooks' => True, + 'fsck' => true, ); /** @@ -101,6 +102,11 @@ class admin_prefs_sidebox_hooks $file['Find and Register all Application Hooks'] = egw::link('/index.php','menuaction=admin.admin_prefs_sidebox_hooks.register_all_hooks'); } + //if (! $GLOBALS['egw']->acl->check('applications_access',16,'admin')) + { + $file['Check virtual filesystem'] = egw::link('/index.php','menuaction=admin.admin_prefs_sidebox_hooks.fsck'); + } + if (! $GLOBALS['egw']->acl->check('asyncservice_access',1,'admin')) { $file['Asynchronous timed services'] = egw::link('/index.php','menuaction=admin.uiasyncservice.index'); @@ -151,4 +157,24 @@ class admin_prefs_sidebox_hooks } $GLOBALS['egw']->redirect_link('/admin/index.php'); } + + /** + * Run fsck on sqlfs + */ + function fsck() + { + $check_only = !isset($_POST['fix']); + + if (!($msgs = sqlfs_utils::fsck($check_only))) + { + $msgs = lang('Filesystem check reported no problems.'); + } + $content = '

'.implode("

\n

", (array)$msgs)."

\n"; + + $content .= html::form('

'.($check_only&&is_array($msgs)?html::submit_button('fix', lang('Fix reported problems')):''). + html::submit_button('cancel', lang('Cancel'), "window.location.href='".egw::link('/admin/index.php')."'; return false;").'

', + '',egw::link('/index.php',array('menuaction'=>'admin.admin_prefs_sidebox_hooks.fsck'))); + + $GLOBALS['egw']->framework->render($content, lang('Admin').' - '.lang('Check virtual filesystem'), true); + } } diff --git a/admin/lang/egw_de.lang b/admin/lang/egw_de.lang index 5b16bd4829..30ecf10c4b 100644 --- a/admin/lang/egw_de.lang +++ b/admin/lang/egw_de.lang @@ -123,6 +123,7 @@ change password hash to admin de Passwort Verschlüsselung ändern zu check acl for entries of not (longer) existing accounts admin de Prüfe ACL Einträge auf Bezüge zu nicht (mehr) existierenden Benutzerkonten check ip address of all sessions admin de IP-Adresse für alle Sessions überprüfen check items to %1 to %2 for %3 admin de Durch Abhaken %3 in %2 %1 +check virtual filesystem admin de Virtuelles Dateisystem überprüfen children admin de Kinder click to select a color admin de Anclicken um eine Farbe auszuwählen color admin de Farbe @@ -277,6 +278,7 @@ false admin de Falsch field '%1' already exists !!! admin de Feld '%1' existiert bereits !!! file space admin de Speicherplatz file space must be an integer admin de Speicherplatz muss eine Zahl sein +filesystem check reported no problems. admin de Überprüfung des Dateisystem ergab keine Probleme. find and register all application hooks admin de Suchen und registrieren der "Hooks" aller Anwendungen for the times above admin de für die oben angegebenen Zeiten for the times below (empty values count as '*', all empty = every minute) admin de für die darunter angegebenen Zeiten (leere Felder zählen als "*", alles leer = jede Minute) diff --git a/admin/lang/egw_en.lang b/admin/lang/egw_en.lang index 645a0c0890..4a0ca1dd92 100644 --- a/admin/lang/egw_en.lang +++ b/admin/lang/egw_en.lang @@ -125,6 +125,7 @@ changed password hash for %1 to %2. admin en Changed password hash for %1 to %2. check acl for entries of not (longer) existing accounts admin en Check ACL for entries of not existing accounts. check ip address of all sessions admin en Check IP address of all sessions check items to %1 to %2 for %3 admin en Check items to %1 to %2 for %3 +check virtual filesystem admin en Check virtual filesystem children admin en Children click to select a color admin en Click to select a color color admin en Color @@ -281,6 +282,7 @@ false admin en False field '%1' already exists !!! admin en Field '%1' already exists! file space admin en File space file space must be an integer admin en File space must be an integer +filesystem check reported no problems. admin en Filesystem check reported no problems. find and register all application hooks admin en Find and register all application hooks for the times above admin en For the times above for the times below (empty values count as '*', all empty = every minute) admin en For the times below: empty values count as '*', all empty = every minute. diff --git a/filemanager/cli.php b/filemanager/cli.php index 64b611a72d..f35e1661d9 100755 --- a/filemanager/cli.php +++ b/filemanager/cli.php @@ -268,7 +268,7 @@ switch($cmd) } die("\n/ NOT mounted with 'storage=db' --> no need to convert!\n\n"); } - $num_files = sqlfs_stream_wrapper::migrate_db2fs(); // throws exception on error + $num_files = sqlfs_utils::migrate_db2fs(); // throws exception on error echo "\n$num_files files migrated from DB to filesystem.\n"; $new_url = preg_replace('/storage=db&?/','',$fstab['/']); if (substr($new_url,-1) == '?') $new_url = substr($new_url,0,-1); diff --git a/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php b/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php index c6bfbcc3b4..a9defc523b 100644 --- a/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php +++ b/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php @@ -1,13 +1,13 @@ - * @copyright (c) 2008-10 by Ralf Becker + * @copyright (c) 2008-12 by Ralf Becker * @version $Id$ */ @@ -729,7 +729,7 @@ class sqlfs_stream_wrapper implements iface_stream_wrapper unset(self::$stat_cache[$path]); $stmt = self::$pdo->prepare('INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified,fs_creator'. ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_size,:fs_mime,:fs_created,:fs_modified,:fs_creator)'); - return $stmt->execute(array( + if (($ok = $stmt->execute(array( 'fs_name' => egw_vfs::basename($path), 'fs_dir' => $parent['ino'], 'fs_mode' => $parent['mode'], @@ -740,7 +740,25 @@ class sqlfs_stream_wrapper implements iface_stream_wrapper 'fs_created' => self::_pdo_timestamp(time()), 'fs_modified' => self::_pdo_timestamp(time()), 'fs_creator' => egw_vfs::$user, - )); + )))) + { + // check if some other process created the directory parallel to us (sqlfs would gives SQL errors later!) + $new_fs_id = self::$pdo->lastInsertId('egw_sqlfs_fs_id_seq'); + + unset($stmt); // free statement object, on some installs a new prepare fails otherwise! + + $stmt = self::$pdo->prepare($q='SELECT COUNT(*) FROM '.self::TABLE. + ' WHERE fs_dir=:fs_dir AND fs_active=:fs_active AND fs_name'.self::$case_sensitive_equal.':fs_name'); + if ($stmt->execute(array( + 'fs_dir' => $parent['ino'], + 'fs_active' => self::_pdo_boolean(true), + 'fs_name' => egw_vfs::basename($path), + )) && $stmt->fetchColumn() > 1) // if there's more then one --> remove our new dir + { + self::$pdo->query('DELETE FROM '.self::TABLE.' WHERE fs_id='.$new_fs_id); + } + } + return $ok; } /** @@ -1841,80 +1859,6 @@ class sqlfs_stream_wrapper implements iface_stream_wrapper if (self::LOG_LEVEL > 1) foreach((array)$props as $k => $v) error_log(__METHOD__."($path_ids,$ns) $k => ".array2string($v)); return $props; } - - /** - * Migrate SQLFS content from DB to filesystem - * - * @param boolean $debug true to echo a message for each copied file - */ - static function migrate_db2fs($debug=false) - { - if (!is_object(self::$pdo)) - { - self::_pdo(); - } - $query = 'SELECT fs_id,fs_name,fs_size,fs_content'. - ' FROM '.self::TABLE.' WHERE fs_content IS NOT NULL'; - - $stmt = self::$pdo->prepare($query); - $stmt->bindColumn(1,$fs_id); - $stmt->bindColumn(2,$fs_name); - $stmt->bindColumn(3,$fs_size); - $stmt->bindColumn(4,$fs_content,PDO::PARAM_LOB); - - if ($stmt->execute()) - { - foreach($stmt as $row) - { - // hack to work around a current php bug (http://bugs.php.net/bug.php?id=40913) - // PDOStatement::bindColumn(,,PDO::PARAM_LOB) is not working for MySQL, content is returned as string :-( - if (is_string($fs_content)) - { - $name = md5($fs_name.$fs_id); - $GLOBALS[$name] =& $fs_content; - require_once(EGW_API_INC.'/class.global_stream_wrapper.inc.php'); - $content = fopen('global://'.$name,'r'); - if (!$content) echo "fopen('global://$name','w' failed, strlen(\$GLOBALS['$name'])=".strlen($GLOBALS[$name]).", \$GLOBALS['$name']=".substr($GLOBALS['$name'],0,100)."...\n"; - unset($GLOBALS[$name]); // unset it, so it does not use up memory, once the stream is closed - } - else - { - $content = $fs_content; - } - if (!is_resource($content)) - { - throw new egw_exception_assertion_failed(__METHOD__."(): fs_id=$fs_id ($fs_name, $fs_size bytes) content is NO resource! ".array2string($content)); - } - $filename = self::_fs_path($fs_id); - if (!file_exists($fs_dir=egw_vfs::dirname($filename))) - { - self::mkdir_recursive($fs_dir,0700,true); - } - if (!($dest = fopen($filename,'w'))) - { - throw new egw_exception_assertion_failed(__METHOD__."(): fopen($filename,'w') failed!"); - } - if (($bytes = stream_copy_to_stream($content,$dest)) != $fs_size) - { - throw new egw_exception_assertion_failed(__METHOD__."(): fs_id=$fs_id ($fs_name) $bytes bytes copied != size of $fs_size bytes!"); - } - if ($debug) echo "$fs_id: $fs_name: $bytes bytes copied to fs\n"; - fclose($dest); - fclose($content); unset($content); - - ++$n; - } - unset($stmt); - - if ($n) // delete all content in DB, if there was some AND no error (exception thrown!) - { - $query = 'UPDATE '.self::TABLE.' SET fs_content=NULL WHERE fs_content IS NOT NULL'; - $stmt = self::$pdo->prepare($query); - $stmt->execute(); - } - } - return $n; - } } stream_register_wrapper(sqlfs_stream_wrapper::SCHEME ,'sqlfs_stream_wrapper'); diff --git a/phpgwapi/inc/class.sqlfs_utils.inc.php b/phpgwapi/inc/class.sqlfs_utils.inc.php new file mode 100644 index 0000000000..b4bc41518c --- /dev/null +++ b/phpgwapi/inc/class.sqlfs_utils.inc.php @@ -0,0 +1,352 @@ + + * @copyright (c) 2008-12 by Ralf Becker + * @version $Id$ + */ + +require_once 'class.iface_stream_wrapper.inc.php'; +require_once 'class.sqlfs_stream_wrapper.inc.php'; + +/** + * sqlfs stream wrapper utilities: migration db-fs, fsck + */ +class sqlfs_utils extends sqlfs_stream_wrapper +{ + /** + * Migrate SQLFS content from DB to filesystem + * + * @param boolean $debug true to echo a message for each copied file + */ + static function migrate_db2fs($debug=false) + { + if (!is_object(self::$pdo)) + { + self::_pdo(); + } + $query = 'SELECT fs_id,fs_name,fs_size,fs_content'. + ' FROM '.self::TABLE.' WHERE fs_content IS NOT NULL'; + + $stmt = self::$pdo->prepare($query); + $stmt->bindColumn(1,$fs_id); + $stmt->bindColumn(2,$fs_name); + $stmt->bindColumn(3,$fs_size); + $stmt->bindColumn(4,$fs_content,PDO::PARAM_LOB); + + if ($stmt->execute()) + { + foreach($stmt as $row) + { + // hack to work around a current php bug (http://bugs.php.net/bug.php?id=40913) + // PDOStatement::bindColumn(,,PDO::PARAM_LOB) is not working for MySQL, content is returned as string :-( + if (is_string($fs_content)) + { + $name = md5($fs_name.$fs_id); + $GLOBALS[$name] =& $fs_content; + require_once(EGW_API_INC.'/class.global_stream_wrapper.inc.php'); + $content = fopen('global://'.$name,'r'); + if (!$content) echo "fopen('global://$name','w' failed, strlen(\$GLOBALS['$name'])=".strlen($GLOBALS[$name]).", \$GLOBALS['$name']=".substr($GLOBALS['$name'],0,100)."...\n"; + unset($GLOBALS[$name]); // unset it, so it does not use up memory, once the stream is closed + } + else + { + $content = $fs_content; + } + if (!is_resource($content)) + { + throw new egw_exception_assertion_failed(__METHOD__."(): fs_id=$fs_id ($fs_name, $fs_size bytes) content is NO resource! ".array2string($content)); + } + $filename = self::_fs_path($fs_id); + if (!file_exists($fs_dir=egw_vfs::dirname($filename))) + { + self::mkdir_recursive($fs_dir,0700,true); + } + if (!($dest = fopen($filename,'w'))) + { + throw new egw_exception_assertion_failed(__METHOD__."(): fopen($filename,'w') failed!"); + } + if (($bytes = stream_copy_to_stream($content,$dest)) != $fs_size) + { + throw new egw_exception_assertion_failed(__METHOD__."(): fs_id=$fs_id ($fs_name) $bytes bytes copied != size of $fs_size bytes!"); + } + if ($debug) echo "$fs_id: $fs_name: $bytes bytes copied to fs\n"; + fclose($dest); + fclose($content); unset($content); + + ++$n; + } + unset($stmt); + + if ($n) // delete all content in DB, if there was some AND no error (exception thrown!) + { + $query = 'UPDATE '.self::TABLE.' SET fs_content=NULL WHERE fs_content IS NOT NULL'; + $stmt = self::$pdo->prepare($query); + $stmt->execute(); + } + } + return $n; + } + + /** + * Check and optionaly fix corruption in sqlfs + * + * @param boolean $check_only=true + * @return array with messages / found problems + */ + public static function fsck($check_only=true) + { + if (!is_object(self::$pdo)) + { + self::_pdo(); + } + $msgs = self::fsck_fix_multiple_active($check_only); + $msgs = array_merge($msgs, self::fsck_fix_unconnected($check_only)); + $msgs = array_merge($msgs, self::fsck_fix_no_content($check_only)); + + return $msgs; + } + + /** + * Check and optionally remove files without content part in physical filesystem + * + * @param boolean $check_only=true + * @return array with messages / found problems + */ + private static function fsck_fix_no_content($check_only=true) + { + $msgs = array(); + + foreach(self::$pdo->query('SELECT fs_id FROM '.self::TABLE. + " WHERE fs_mime!='httpd/unix-directory' AND fs_content IS NULL AND fs_link IS NULL") as $row) + { + if (!file_exists($phy_path=self::_fs_path($row['fs_id']))) + { + egw_vfs::$is_root = true; + $path = self::id2path($row['fs_id']); + if ($check_only) + { + $msgs[] = lang('File %1 has no content in physical filesystem %2!', + $path.' (#'.$row['fs_id'].')',$phy_path); + } + elseif (self::unlink($path.'?storage=db')) // storage=db to not try to delete not existing phy. file + { + $msgs[] = lang('File %1 has no content in physical filesystem %2 --> file removed!',$path,$phy_path); + } + else + { + $msgs[] = lang('File %1 has no content in physical filesystem %2 --> failed to remove file!', + $path.' (#'.$row['fs_id'].')',$phy_path); + } + egw_vfs::$is_root = false; + } + } + if ($check_only && $msgs) + { + $msgs[] = lang('Files without content in physical filesystem will be removed.'); + } + return $msgs; + } + + /** + * Name of lost+found directory for unconnected nodes + */ + const LOST_N_FOUND = '/lost+found'; + const LOST_N_FOUND_MOD = 070; + const LOST_N_FOUND_GRP = 'Admins'; + + /** + * Check and optionally fix unconnected nodes - parent directory does not (longer) exists: + * + * SELECT fs.* + * FROM egw_sqlfs fs + * LEFT JOIN egw_sqlfs dir ON dir.fs_id=fs.fs_dir + * WHERE fs.fs_id > 1 && dir.fs_id IS NULL + * + * @param boolean $check_only=true + * @return array with messages / found problems + */ + private static function fsck_fix_unconnected($check_only=true) + { + $msgs = array(); + foreach(self::$pdo->query('SELECT fs.* FROM '.self::TABLE.' fs'. + ' LEFT JOIN '.self::TABLE.' dir ON dir.fs_id=fs.fs_dir'. + ' WHERE fs.fs_id > 1 && dir.fs_id IS NULL') as $row) + { + if ($check_only) + { + $msgs[] = lang('Found unconnected %1 %2!', + mime_magic::mime2label($row['fs_mime']), + egw_vfs::decodePath($row['fs_name']).' (#'.$row['fs_id'].')'); + continue; + } + if (!isset($lostnfound)) + { + // check if we already have /lost+found, create it if not + if (!($lostnfound = self::url_stat(self::LOST_N_FOUND, STREAM_URL_STAT_QUIET))) + { + egw_vfs::$is_root = true; + if (!self::mkdir(self::LOST_N_FOUND, self::LOST_N_FOUND_MOD, 0) || + !(!($admins = $GLOBALS['egw']->accounts->name2id(self::LOST_N_FOUND_GRP)) || + self::chgrp(self::LOST_N_FOUND, $admins) && self::chmod(self::LOST_N_FOUND,self::LOST_N_FOUND_MOD)) || + !($lostnfound = self::url_stat(self::LOST_N_FOUND, STREAM_URL_STAT_QUIET))) + { + $msgs[] = lang("Can't create directory %1 to connect found unconnected nodes to it!",self::LOST_N_FOUND); + } + else + { + $msgs[] = lang('Successful created new directory %1 for unconnected nods.',self::LOST_N_FOUND); + } + egw_vfs::$is_root = false; + if (!$lostnfound) break; + } + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_dir=:fs_dir WHERE fs_id=:fs_id'); + } + if ($stmt->execute(array( + 'fs_dir' => $lostnfound['ino'], + 'fs_id' => $row['fs_id'], + ))) + { + $msgs[] = lang('Moved unconnected %1 %2 to %3.', + mime_magic::mime2label($row['fs_mime']), + egw_vfs::decodePath($row['fs_name']).' (#'.$row['fs_id'].')', + self::LOST_N_FOUND); + } + else + { + $msgs[] = lang('Faild to move unconnected %1 %2 to %3!', + mime_magic::mime2label($row['fs_mime']), egw_vfs::decodePath($row['fs_name']), self::LOST_N_FOUND); + } + } + if ($check_only && $msgs) + { + $msgs[] = lang('Unconnected nodes will be moved to %1.',self::LOST_N_FOUND); + continue; + } + return $msgs; + } + + /** + * Check and optionally fix multiple active files and directories with identical path + * + * @param boolean $check_only=true + * @return array with messages / found problems + */ + private static function fsck_fix_multiple_active($check_only=true) + { + $msgs = array(); + foreach(self::$pdo->query('SELECT fs_dir,fs_name,COUNT(*) FROM '.self::TABLE. + ' WHERE fs_active='.self::_pdo_boolean(true). + ' GROUP BY fs_dir,fs_name'. + ' HAVING COUNT(*) > 1') as $row) + { + if (!isset($stmt)) + { + $stmt = self::$pdo->prepare('SELECT *,(SELECT COUNT(*) FROM '.self::TABLE.' sub WHERE sub.fs_dir=fs.fs_id) AS children'. + ' FROM '.self::TABLE.' fs'. + ' WHERE fs.fs_dir=:fs_dir AND fs.fs_active='.self::_pdo_boolean(true).' AND fs.fs_name'.self::$case_sensitive_equal.':fs_name'. + " ORDER BY fs.fs_mime='httpd/unix-directory' DESC,children DESC,fs.fs_modified DESC"); + $inactivate_stmt = self::$pdo->prepare('UPDATE '.self::TABLE. + ' SET fs_active='.self::_pdo_boolean(false). + ' WHERE fs_dir=:fs_dir AND fs_active='.self::_pdo_boolean(true). + ' AND fs_name'.self::$case_sensitive_equal.':fs_name AND fs_id!=:fs_id'); + } + //$msgs[] = array2string($row); + $cnt = 0; + $stmt->execute(array( + 'fs_dir' => $row['fs_dir'], + 'fs_name' => $row['fs_name'], + )); + foreach($stmt as $entry) + { + if ($entry['fs_mime'] == 'httpd/unix-directory') + { + if (!$n) + { + $dir = $entry; // directory to keep + $msgs[] = lang('%1 directories %2 found!', $row[2], self::id2path($entry['fs_id'])); + if ($check_only) break; + } + else + { + if ($entry['children']) + { + $msgs[] = lang('Moved %1 children from directory fs_id=%2 to %3', + $children = self::$pdo->exec('UPDATE '.self::TABLE.' SET fs_dir='.(int)$dir['fs_id']. + ' WHERE fs_dir='.(int)$entry['fs_id']), + $entry['fs_id'], $dir['fs_id']); + + $dir['children'] += $children; + } + self::$pdo->query('DELETE FROM '.self::TABLE.' WHERE fs_id='.(int)$entry['fs_id']); + $msgs[] = lang('Removed (now) empty directory fs_id=%1',$entry['fs_id']); + } + } + elseif (isset($dir)) // file and directory with same name exist! + { + if (!$check_only) + { + $inactivate_stmt->execute(array( + 'fs_dir' => $row['fs_dir'], + 'fs_name' => $row['fs_name'], + 'fs_id' => $dir['fs_id'], + )); + $cnt = $inactivate_stmt->rowCount(); + } + else + { + $cnt = ucfirst(lang('none of %1', $row[2]-1)); + } + $msgs[] = lang('%1 active file(s) with same name as directory inactivated!',$cnt); + break; + } + else // newest file --> set for all other fs_active=false + { + if (!$check_only) + { + $inactivate_stmt->execute(array( + 'fs_dir' => $row['fs_dir'], + 'fs_name' => $row['fs_name'], + 'fs_id' => $entry['fs_id'], + )); + $cnt = $inactivate_stmt->rowCount(); + } + else + { + $cnt = lang('none of %1', $row[2]-1); + } + $msgs[] = lang('More then one active file %1 found, inactivating %2 older revisions!', + self::id2path($entry['fs_id']), $cnt); + break; + } + } + unset($dir); + if ($cnt && !isset($inactivate_msg_added)) + { + $msgs[] = lang('To examine or reinstate inactived files, you might need to turn versioning on.'); + $inactivate_msg_added = true; + } + } + return $msgs; + } +} + +// fsck testcode, if this file is called via it's URL (you need to uncomment it!) +/*if (isset($_SERVER['SCRIPT_FILENAME']) && $_SERVER['SCRIPT_FILENAME'] == __FILE__) +{ + $GLOBALS['egw_info'] = array( + 'flags' => array( + 'currentapp' => 'admin', + 'nonavbar' => true, + ), + ); + include_once '../../header.inc.php'; + + $msgs = sqlfs_utils::fsck(!isset($_GET['check_only']) || $_GET['check_only']); + echo '

'.implode("

\n

", (array)$msgs)."

\n"; +}*/ \ No newline at end of file