PageRenderTime 1251ms CodeModel.GetById 36ms RepoModel.GetById 0ms app.codeStats 0ms

/includes/common.php

https://bitbucket.org/lordgnu/greyhole
PHP | 1537 lines | 1438 code | 67 blank | 32 comment | 162 complexity | 9cc57d5546ce8ef4ab25a605a1e15c74 MD5 | raw file
  1. <?php
  2. /*
  3. Copyright 2009-2011 Guillaume Boudreau, Andrew Hopkinson
  4. This file is part of Greyhole.
  5. Greyhole is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. Greyhole is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with Greyhole. If not, see <http://www.gnu.org/licenses/>.
  15. */
  16. define('PERF', 9);
  17. define('TEST', 8);
  18. define('DEBUG', 7);
  19. define('INFO', 6);
  20. define('WARN', 4);
  21. define('ERROR', 3);
  22. define('CRITICAL', 2);
  23. $action = 'initialize';
  24. date_default_timezone_set(date_default_timezone_get());
  25. set_error_handler("gh_error_handler");
  26. register_shutdown_function("gh_shutdown");
  27. umask(0);
  28. setlocale(LC_COLLATE, "en_US.UTF-8");
  29. setlocale(LC_CTYPE, "en_US.UTF-8");
  30. if (!defined('PHP_VERSION_ID')) {
  31. $version = explode('.', PHP_VERSION);
  32. define('PHP_VERSION_ID', ($version[0] * 10000 + $version[1] * 100 + $version[2]));
  33. }
  34. $constarray = get_defined_constants(true);
  35. foreach($constarray['user'] as $key => $val) {
  36. eval(sprintf('$_CONSTANTS[\'%s\'] = ' . (is_int($val) || is_float($val) ? '%s' : "'%s'") . ';', addslashes($key), addslashes($val)));
  37. }
  38. // Cached df results
  39. $last_df_time = 0;
  40. $last_dfs = array();
  41. $sleep_before_task = array();
  42. if (!isset($config_file)) {
  43. $config_file = '/etc/greyhole.conf';
  44. }
  45. if (!isset($smb_config_file)) {
  46. $smb_config_file = '/etc/samba/smb.conf';
  47. }
  48. $trash_share_names = array('Greyhole Attic', 'Greyhole Trash', 'Greyhole Recycle Bin');
  49. function recursive_include_parser($file) {
  50. $regex = '/^[ \t]*include[ \t]*=[ \t]*([^#\r\n]+)/im';
  51. $ok_to_execute = FALSE;
  52. if (is_array($file) && count($file) > 1) {
  53. $file = $file[1];
  54. }
  55. $file = trim($file);
  56. if (file_exists($file)) {
  57. if (is_executable($file)) {
  58. $perms = fileperms($file);
  59. // Not user-writable, or owned by root
  60. $ok_to_execute = !($perms & 0x0080) || fileowner($file) === 0;
  61. // Not group-writable, or group owner is root
  62. $ok_to_execute &= !($perms & 0x0010) || filegroup($file) === 0;
  63. // Not world-writable
  64. $ok_to_execute &= !($perms & 0x0002);
  65. if (!$ok_to_execute) {
  66. gh_log(WARN, "Config file '{$file}' is executable but file permissions are insecure, only the file's contents will be included.");
  67. }
  68. }
  69. $contents = $ok_to_execute ? shell_exec(escapeshellcmd($file)) : file_get_contents($file);
  70. return preg_replace_callback($regex, 'recursive_include_parser', $contents);
  71. } else {
  72. return false;
  73. }
  74. }
  75. function parse_config() {
  76. global $_CONSTANTS, $log_level, $storage_pool_drives, $shares_options, $minimum_free_space_pool_drives, $df_command, $config_file, $smb_config_file, $sticky_files, $db_options, $frozen_directories, $trash_share_names, $max_queued_tasks, $memory_limit, $delete_moves_to_trash, $greyhole_log_file, $email_to, $log_memory_usage, $check_for_open_files, $allow_multiple_sp_per_device, $df_cache_time;
  77. $deprecated_options = array(
  78. 'delete_moves_to_attic' => 'delete_moves_to_trash',
  79. 'storage_pool_directory' => 'storage_pool_drive',
  80. 'dir_selection_groups' => 'drive_selection_groups',
  81. 'dir_selection_algorithm' => 'drive_selection_algorithm'
  82. );
  83. $parsing_drive_selection_groups = FALSE;
  84. $shares_options = array();
  85. $storage_pool_drives = array();
  86. $frozen_directories = array();
  87. $config_text = recursive_include_parser($config_file);
  88. // Defaults
  89. $log_level = DEBUG;
  90. $greyhole_log_file = '/var/log/greyhole.log';
  91. $email_to = 'root';
  92. $log_memory_usage = FALSE;
  93. $check_for_open_files = TRUE;
  94. $allow_multiple_sp_per_device = FALSE;
  95. $df_cache_time = 15;
  96. $delete_moves_to_trash = TRUE;
  97. $memory_limit = '128M';
  98. foreach (explode("\n", $config_text) as $line) {
  99. if (preg_match("/^[ \t]*([^=\t]+)[ \t]*=[ \t]*([^#]+)/", $line, $regs)) {
  100. $name = trim($regs[1]);
  101. $value = trim($regs[2]);
  102. if ($name[0] == '#') {
  103. continue;
  104. }
  105. foreach ($deprecated_options as $old_name => $new_name) {
  106. if (mb_strpos($name, $old_name) !== FALSE) {
  107. $fixed_name = str_replace($old_name, $new_name, $name);
  108. #gh_log(WARN, "Deprecated option found in greyhole.conf: $name. You should change that to: $fixed_name");
  109. $name = $fixed_name;
  110. }
  111. }
  112. $parsing_drive_selection_groups = FALSE;
  113. switch($name) {
  114. case 'log_level':
  115. $log_level = $_CONSTANTS[$value];
  116. break;
  117. case 'delete_moves_to_trash': // or delete_moves_to_attic
  118. case 'log_memory_usage':
  119. case 'check_for_open_files':
  120. case 'allow_multiple_sp_per_device':
  121. global ${$name};
  122. ${$name} = trim($value) === '1' || mb_stripos($value, 'yes') !== FALSE || mb_stripos($value, 'true') !== FALSE;
  123. break;
  124. case 'storage_pool_drive': // or storage_pool_directory
  125. if (preg_match("/(.*) ?, ?min_free ?: ?([0-9]+) ?([gmk])b?/i", $value, $regs)) {
  126. $storage_pool_drives[] = trim($regs[1]);
  127. if (strtolower($regs[3]) == 'g') {
  128. $minimum_free_space_pool_drives[trim($regs[1])] = (float) trim($regs[2]);
  129. } else if (strtolower($regs[3]) == 'm') {
  130. $minimum_free_space_pool_drives[trim($regs[1])] = (float) trim($regs[2]) / 1024.0;
  131. } else if (strtolower($regs[3]) == 'k') {
  132. $minimum_free_space_pool_drives[trim($regs[1])] = (float) trim($regs[2]) / 1024.0 / 1024.0;
  133. }
  134. }
  135. break;
  136. case 'wait_for_exclusive_file_access':
  137. $shares = explode(',', str_replace(' ', '', $value));
  138. foreach ($shares as $share) {
  139. $shares_options[$share]['wait_for_exclusive_file_access'] = TRUE;
  140. }
  141. break;
  142. case 'sticky_files':
  143. $last_sticky_files_dir = trim($value, '/');
  144. $sticky_files[$last_sticky_files_dir] = array();
  145. break;
  146. case 'stick_into':
  147. $sticky_files[$last_sticky_files_dir][] = '/' . trim($value, '/');
  148. break;
  149. case 'frozen_directory':
  150. $frozen_directories[] = trim($value, '/');
  151. break;
  152. case 'memory_limit':
  153. ini_set('memory_limit',$value);
  154. $memory_limit = $value;
  155. break;
  156. case 'drive_selection_groups': // or dir_selection_groups
  157. if (preg_match("/(.+):(.*)/", $value, $regs)) {
  158. global $drive_selection_groups;
  159. $group_name = trim($regs[1]);
  160. $drive_selection_groups[$group_name] = array_map('trim', explode(',', $regs[2]));
  161. $parsing_drive_selection_groups = TRUE;
  162. }
  163. break;
  164. case 'drive_selection_algorithm': // or dir_selection_algorithm
  165. global $drive_selection_algorithm;
  166. $drive_selection_algorithm = DriveSelection::parse($value, @$drive_selection_groups);
  167. break;
  168. default:
  169. if (mb_strpos($name, 'num_copies') === 0) {
  170. $share = mb_substr($name, 11, mb_strlen($name)-12);
  171. if (mb_stripos($value, 'max') === 0) {
  172. $value = 9999;
  173. }
  174. $shares_options[$share]['num_copies'] = (int) $value;
  175. } else if (mb_strpos($name, 'delete_moves_to_trash') === 0) {
  176. $share = mb_substr($name, 22, mb_strlen($name)-23);
  177. $shares_options[$share]['delete_moves_to_trash'] = trim($value) === '1' || mb_strpos(strtolower(trim($value)), 'yes') !== FALSE || mb_strpos(strtolower(trim($value)), 'true') !== FALSE;
  178. } else if (mb_strpos($name, 'drive_selection_groups') === 0) { // or dir_selection_groups
  179. $share = mb_substr($name, 23, mb_strlen($name)-24);
  180. if (preg_match("/(.+):(.+)/", $value, $regs)) {
  181. $group_name = trim($regs[1]);
  182. $shares_options[$share]['drive_selection_groups'][$group_name] = array_map('trim', explode(',', $regs[2]));
  183. $parsing_drive_selection_groups = $share;
  184. }
  185. } else if (mb_strpos($name, 'drive_selection_algorithm') === 0) { // or dir_selection_algorithm
  186. $share = mb_substr($name, 26, mb_strlen($name)-27);
  187. if (!isset($shares_options[$share]['drive_selection_groups'])) {
  188. $shares_options[$share]['drive_selection_groups'] = @$drive_selection_groups;
  189. }
  190. $shares_options[$share]['drive_selection_algorithm'] = DriveSelection::parse($value, $shares_options[$share]['drive_selection_groups']);
  191. } else {
  192. global ${$name};
  193. if (is_numeric($value)) {
  194. ${$name} = (int) $value;
  195. } else {
  196. ${$name} = $value;
  197. }
  198. }
  199. }
  200. } else if ($parsing_drive_selection_groups !== FALSE) {
  201. $value = trim($line);
  202. if (strlen($value) == 0 || $value[0] == '#') {
  203. continue;
  204. }
  205. if (preg_match("/(.+):(.+)/", $value, $regs)) {
  206. $group_name = trim($regs[1]);
  207. $drives = array_map('trim', explode(',', $regs[2]));
  208. if (is_string($parsing_drive_selection_groups)) {
  209. $share = $parsing_drive_selection_groups;
  210. $shares_options[$share]['drive_selection_groups'][$group_name] = $drives;
  211. } else {
  212. $drive_selection_groups[$group_name] = $drives;
  213. }
  214. }
  215. }
  216. }
  217. if (is_array($storage_pool_drives) && count($storage_pool_drives) > 0) {
  218. $df_command = "df -k";
  219. foreach ($storage_pool_drives as $key => $sp_drive) {
  220. $df_command .= " " . escapeshellarg($sp_drive);
  221. $storage_pool_drives[$key] = '/' . trim($sp_drive, '/');
  222. }
  223. $df_command .= " 2>&1 | grep '%' | grep -v \"^df: .*: No such file or directory$\"";
  224. } else {
  225. gh_log(WARN, "You have no storage_pool_drive defined. Greyhole can't run.");
  226. return FALSE;
  227. }
  228. exec('testparm -s ' . escapeshellarg($smb_config_file) . ' 2> /dev/null', $config_text);
  229. foreach ($config_text as $line) {
  230. $line = trim($line);
  231. if (mb_strlen($line) == 0) { continue; }
  232. if ($line[0] == '[' && preg_match('/\[([^\]]+)\]/', $line, $regs)) {
  233. $share_name = $regs[1];
  234. }
  235. if (isset($share_name) && !isset($shares_options[$share_name]) && array_search($share_name, $trash_share_names) === FALSE) { continue; }
  236. if (isset($share_name) && preg_match('/^\s*path[ \t]*=[ \t]*(.+)$/i', $line, $regs)) {
  237. $shares_options[$share_name]['landing_zone'] = '/' . trim($regs[1], '/');
  238. $shares_options[$share_name]['name'] = $share_name;
  239. }
  240. }
  241. global $drive_selection_algorithm;
  242. if (isset($drive_selection_algorithm)) {
  243. foreach ($drive_selection_algorithm as $ds) {
  244. $ds->update();
  245. }
  246. } else {
  247. // Default drive_selection_algorithm
  248. $drive_selection_algorithm = DriveSelection::parse('most_available_space', null);
  249. }
  250. foreach ($shares_options as $share_name => $share_options) {
  251. if (array_search($share_name, $trash_share_names) !== FALSE) {
  252. global $trash_share;
  253. $trash_share = array('name' => $share_name, 'landing_zone' => $shares_options[$share_name]['landing_zone']);
  254. unset($shares_options[$share_name]);
  255. continue;
  256. }
  257. if ($share_options['num_copies'] > count($storage_pool_drives)) {
  258. $share_options['num_copies'] = count($storage_pool_drives);
  259. }
  260. if (!isset($share_options['landing_zone'])) {
  261. global $config_file, $smb_config_file;
  262. gh_log(WARN, "Found a share ($share_name) defined in $config_file with no path in $smb_config_file. Either add this share in $smb_config_file, or remove it from $config_file, then restart Greyhole.");
  263. return FALSE;
  264. }
  265. if (!isset($share_options['delete_moves_to_trash'])) {
  266. $share_options['delete_moves_to_trash'] = $delete_moves_to_trash;
  267. }
  268. if (isset($share_options['drive_selection_algorithm'])) {
  269. foreach ($share_options['drive_selection_algorithm'] as $ds) {
  270. $ds->update();
  271. }
  272. } else {
  273. $share_options['drive_selection_algorithm'] = $drive_selection_algorithm;
  274. }
  275. if (isset($share_options['drive_selection_groups'])) {
  276. unset($share_options['drive_selection_groups']);
  277. }
  278. $shares_options[$share_name] = $share_options;
  279. // Validate that the landing zone is NOT a subdirectory of a storage pool drive!
  280. foreach ($storage_pool_drives as $key => $sp_drive) {
  281. if (mb_strpos($share_options['landing_zone'], $sp_drive) === 0) {
  282. gh_log(CRITICAL, "Found a share ($share_name), with path " . $share_options['landing_zone'] . ", which is INSIDE a storage pool drive ($sp_drive). Share directories should never be inside a directory that you have in your storage pool.\nFor your shares to use your storage pool, you just need them to have 'vfs objects = greyhole' in their (smb.conf) config; their location on your file system is irrelevant.");
  283. }
  284. }
  285. }
  286. if (!isset($db_engine)) {
  287. $db_engine = 'mysql';
  288. } else {
  289. $db_engine = mb_strtolower($db_engine);
  290. }
  291. global ${"db_use_$db_engine"};
  292. ${"db_use_$db_engine"} = TRUE;
  293. if (!isset($max_queued_tasks)) {
  294. if ($db_engine == 'sqlite') {
  295. $max_queued_tasks = 1000;
  296. } else {
  297. $max_queued_tasks = 10000000;
  298. }
  299. }
  300. if (!isset($memory_limit)) {
  301. ini_set('memory_limit', $memory_limit);
  302. }
  303. if (isset($memory_limit)) {
  304. if(preg_match('/M$/',$memory_limit)) {
  305. $memory_limit = preg_replace('/M$/','',$memory_limit);
  306. $memory_limit = $memory_limit * 1048576;
  307. }elseif(preg_match('/K$/',$memory_limit)) {
  308. $memory_limit = preg_replace('/K$/','',$memory_limit);
  309. $memory_limit = $memory_limit * 1024;
  310. }
  311. }
  312. $db_options = (object) array(
  313. 'engine' => $db_engine,
  314. 'schema' => "/usr/share/greyhole/schema-$db_engine.sql"
  315. );
  316. if ($db_options->engine == 'sqlite') {
  317. $db_options->db_path = $db_path;
  318. $db_options->dbh = null; // internal handle to use with sqlite
  319. } else {
  320. $db_options->host = $db_host;
  321. $db_options->user = $db_user;
  322. $db_options->pass = $db_pass;
  323. $db_options->name = $db_name;
  324. }
  325. include('includes/sql.php');
  326. if (strtolower($greyhole_log_file) == 'syslog') {
  327. openlog("Greyhole", LOG_PID, LOG_USER);
  328. }
  329. return TRUE;
  330. }
  331. function clean_dir($dir) {
  332. if ($dir[0] == '.' && $dir[1] == '/') {
  333. $dir = mb_substr($dir, 2);
  334. }
  335. while (mb_strpos($dir, '//') !== FALSE) {
  336. $dir = str_replace("//", "/", $dir);
  337. }
  338. return $dir;
  339. }
  340. function explode_full_path($full_path) {
  341. return array(dirname($full_path), basename($full_path));
  342. }
  343. function gh_log($local_log_level, $text) {
  344. global $greyhole_log_file, $log_level, $log_memory_usage, $action, $log_to_stdout;
  345. if ($local_log_level > $log_level) {
  346. return;
  347. }
  348. $date = date("M d H:i:s");
  349. if ($log_level >= PERF) {
  350. $utimestamp = microtime(true);
  351. $timestamp = floor($utimestamp);
  352. $date .= '.' . round(($utimestamp - $timestamp) * 1000000);
  353. }
  354. $log_text = sprintf("%s%s%s\n",
  355. "$date $local_log_level $action: ",
  356. $text,
  357. @$log_memory_usage ? " [" . memory_get_usage() . "]" : ''
  358. );
  359. if (isset($log_to_stdout)) {
  360. echo $log_text;
  361. } else {
  362. if (strtolower($greyhole_log_file) == 'syslog') {
  363. $worked = syslog($local_log_level, $log_text);
  364. } else if (!empty($greyhole_log_file)) {
  365. $worked = error_log($log_text, 3, $greyhole_log_file);
  366. } else {
  367. $worked = FALSE;
  368. }
  369. if (!$worked) {
  370. error_log(trim($log_text));
  371. }
  372. }
  373. if ($local_log_level === CRITICAL) {
  374. exit(1);
  375. }
  376. }
  377. function gh_shutdown() {
  378. if ($err = error_get_last()) {
  379. gh_log(ERROR, "PHP Fatal Error: " . $err['message'] . "; BT: " . basename($err['file']) . '[L' . $err['line'] . '] ');
  380. }
  381. }
  382. function gh_error_handler($errno, $errstr, $errfile, $errline, $errcontext) {
  383. if (error_reporting() === 0) {
  384. // Ignored (@) warning
  385. return TRUE;
  386. }
  387. switch ($errno) {
  388. case E_ERROR:
  389. case E_PARSE:
  390. case E_CORE_ERROR:
  391. case E_COMPILE_ERROR:
  392. gh_log(CRITICAL, "PHP Error [$errno]: $errstr in $errfile on line $errline");
  393. break;
  394. case E_WARNING:
  395. case E_COMPILE_WARNING:
  396. case E_CORE_WARNING:
  397. case E_NOTICE:
  398. global $greyhole_log_file;
  399. if ($errstr == "fopen($greyhole_log_file): failed to open stream: Permission denied") {
  400. // We want to ignore this warning. Happens when regular users try to use greyhole, and greyhole tries to log something.
  401. // What would have been logged will be echoed instead.
  402. return TRUE;
  403. }
  404. gh_log(WARN, "PHP Warning [$errno]: $errstr in $errfile on line $errline; BT: " . get_debug_bt());
  405. break;
  406. default:
  407. gh_log(WARN, "PHP Unknown Error [$errno]: $errstr in $errfile on line $errline");
  408. break;
  409. }
  410. // Don't execute PHP internal error handler
  411. return TRUE;
  412. }
  413. function get_debug_bt() {
  414. $bt = '';
  415. foreach (debug_backtrace() as $d) {
  416. if ($d['function'] == 'gh_error_handler' || $d['function'] == 'get_debug_bt') { continue; }
  417. if ($bt != '') {
  418. $bt = " => $bt";
  419. }
  420. $prefix = '';
  421. if (isset($d['file'])) {
  422. $prefix = basename($d['file']) . '[L' . $d['line'] . '] ';
  423. }
  424. foreach ($d['args'] as $k => $v) {
  425. if (is_object($v)) {
  426. $d['args'][$k] = 'stdClass';
  427. }
  428. if (is_array($v)) {
  429. $d['args'][$k] = str_replace("\n", "", var_export($v, TRUE));
  430. }
  431. }
  432. $bt = $prefix . $d['function'] .'(' . implode(',', $d['args']) . ')' . $bt;
  433. }
  434. return $bt;
  435. }
  436. function bytes_to_human($bytes, $html=TRUE) {
  437. $units = 'B';
  438. if (abs($bytes) > 1024) {
  439. $bytes /= 1024;
  440. $units = 'KB';
  441. }
  442. if (abs($bytes) > 1024) {
  443. $bytes /= 1024;
  444. $units = 'MB';
  445. }
  446. if (abs($bytes) > 1024) {
  447. $bytes /= 1024;
  448. $units = 'GB';
  449. }
  450. if (abs($bytes) > 1024) {
  451. $bytes /= 1024;
  452. $units = 'TB';
  453. }
  454. $decimals = (abs($bytes) > 100 ? 0 : (abs($bytes) > 10 ? 1 : 2));
  455. if ($html) {
  456. return number_format($bytes, $decimals) . " <span class=\"i18n-$units\">$units</span>";
  457. } else {
  458. return number_format($bytes, $decimals) . $units;
  459. }
  460. }
  461. function duration_to_human($seconds) {
  462. $displayable_duration = '';
  463. if ($seconds > 60*60) {
  464. $hours = floor($seconds / (60*60));
  465. $displayable_duration .= $hours . 'h ';
  466. $seconds -= $hours * (60*60);
  467. }
  468. if ($seconds > 60) {
  469. $minutes = floor($seconds / 60);
  470. $displayable_duration .= $minutes . 'm ';
  471. $seconds -= $minutes * 60;
  472. }
  473. $displayable_duration .= $seconds . 's';
  474. return $displayable_duration;
  475. }
  476. function get_share_landing_zone($share) {
  477. global $shares_options, $trash_share_names;
  478. if (isset($shares_options[$share]['landing_zone'])) {
  479. return $shares_options[$share]['landing_zone'];
  480. } else if (array_search($share, $trash_share_names) !== FALSE) {
  481. global $trash_share;
  482. return $trash_share['landing_zone'];
  483. } else {
  484. global $config_file, $smb_config_file;
  485. gh_log(WARN, " Found a share ($share) with no path in $smb_config_file, or missing it's num_copies[$share] config in $config_file. Skipping.");
  486. return FALSE;
  487. }
  488. }
  489. $arch = exec('uname -m');
  490. if ($arch != 'x86_64') {
  491. gh_log(DEBUG, "32-bit system detected: Greyhole will NOT use PHP built-in file functions.");
  492. function gh_filesize($filename) {
  493. $result = exec("stat -c %s ".escapeshellarg($filename)." 2>/dev/null");
  494. if (empty($result)) {
  495. return FALSE;
  496. }
  497. return (float) $result;
  498. }
  499. function gh_fileowner($filename) {
  500. $result = exec("stat -c %u ".escapeshellarg($filename)." 2>/dev/null");
  501. if (empty($result)) {
  502. return FALSE;
  503. }
  504. return (int) $result;
  505. }
  506. function gh_filegroup($filename) {
  507. $result = exec("stat -c %g ".escapeshellarg($filename)." 2>/dev/null");
  508. if (empty($result)) {
  509. return FALSE;
  510. }
  511. return (int) $result;
  512. }
  513. function gh_fileperms($filename) {
  514. $result = exec("stat -c %a ".escapeshellarg($filename)." 2>/dev/null");
  515. if (empty($result)) {
  516. return FALSE;
  517. }
  518. return "0" . $result;
  519. }
  520. function gh_is_file($filename) {
  521. exec('[ -f '.escapeshellarg($filename).' ]', $tmp, $result);
  522. return $result === 0;
  523. }
  524. function gh_fileinode($filename) {
  525. // This function returns deviceid_inode to make sure this value will be different for files on different devices.
  526. $result = exec("stat -c '%d_%i' ".escapeshellarg($filename)." 2>/dev/null");
  527. if (empty($result)) {
  528. return FALSE;
  529. }
  530. return (string) $result;
  531. }
  532. function gh_file_deviceid($filename) {
  533. $result = exec("stat -c '%d' ".escapeshellarg($filename)." 2>/dev/null");
  534. if (empty($result)) {
  535. return FALSE;
  536. }
  537. return (string) $result;
  538. }
  539. function gh_rename($filename, $target_filename) {
  540. exec("mv ".escapeshellarg($filename)." ".escapeshellarg($target_filename)." 2>/dev/null", $output, $result);
  541. return $result === 0;
  542. }
  543. } else {
  544. gh_log(DEBUG, "64-bit system detected: Greyhole will use PHP built-in file functions.");
  545. function gh_filesize($filename) {
  546. return filesize($filename);
  547. }
  548. function gh_fileowner($filename) {
  549. return fileowner($filename);
  550. }
  551. function gh_filegroup($filename) {
  552. return filegroup($filename);
  553. }
  554. function gh_fileperms($filename) {
  555. return mb_substr(decoct(fileperms($filename)), -4);
  556. }
  557. function gh_is_file($filename) {
  558. return is_file($filename);
  559. }
  560. function gh_fileinode($filename) {
  561. // This function returns deviceid_inode to make sure this value will be different for files on different devices.
  562. $stat = @stat($filename);
  563. if ($stat === FALSE) {
  564. return FALSE;
  565. }
  566. return $stat['dev'] . '_' . $stat['ino'];
  567. }
  568. function gh_file_deviceid($filename) {
  569. $stat = @stat($filename);
  570. if ($stat === FALSE) {
  571. return FALSE;
  572. }
  573. return $stat['dev'];
  574. }
  575. function gh_rename($filename, $target_filename) {
  576. return @rename($filename, $target_filename);
  577. }
  578. }
  579. function memory_check() {
  580. global $memory_limit;
  581. $usage = memory_get_usage();
  582. $used = $usage/$memory_limit;
  583. $used = $used * 100;
  584. if ($used > 95) {
  585. gh_log(CRITICAL, $used . '% memory usage, exiting. Please increase memory_limit in /etc/greyhole.conf');
  586. }
  587. }
  588. class metafile_iterator implements Iterator {
  589. private $path;
  590. private $share;
  591. private $load_nok_metafiles;
  592. private $quiet;
  593. private $check_symlink;
  594. private $metafiles;
  595. private $metastores;
  596. private $dir_handle;
  597. public function __construct($share, $path, $load_nok_metafiles=FALSE, $quiet=FALSE, $check_symlink=TRUE) {
  598. $this->quiet = $quiet;
  599. $this->share = $share;
  600. $this->path = $path;
  601. $this->check_symlink = $check_symlink;
  602. $this->load_nok_metafiles = $load_nok_metafiles;
  603. }
  604. public function rewind() {
  605. $this->metastores = get_metastores();
  606. $this->directory_stack = array($this->path);
  607. $this->dir_handle = NULL;
  608. $this->metafiles = array();
  609. $this->next();
  610. }
  611. public function current() {
  612. return $this->metafiles;
  613. }
  614. public function key() {
  615. return count($this->metafiles);
  616. }
  617. public function next() {
  618. $this->metafiles = array();
  619. while(count($this->directory_stack)>0 && $this->directory_stack !== NULL) {
  620. $this->dir = array_pop($this->directory_stack);
  621. if (!$this->quiet) {
  622. gh_log(DEBUG, "Loading metadata files for (dir) " . clean_dir($this->share . (!empty($this->dir) ? "/" . $this->dir : "")) . " ...");
  623. }
  624. for( $i = 0; $i < count($this->metastores); $i++ ) {
  625. $metastore = $this->metastores[$i];
  626. $this->base = "$metastore/".$this->share."/";
  627. if(!file_exists($this->base.$this->dir)) {
  628. continue;
  629. }
  630. if($this->dir_handle = opendir($this->base.$this->dir)) {
  631. while (false !== ($file = readdir($this->dir_handle))) {
  632. memory_check();
  633. if($file=='.' || $file=='..')
  634. continue;
  635. if(!empty($this->dir)) {
  636. $full_filename = $this->dir . '/' . $file;
  637. }else
  638. $full_filename = $file;
  639. if(is_dir($this->base.$full_filename))
  640. $this->directory_stack[] = $full_filename;
  641. else{
  642. $full_filename = str_replace("$this->path/",'',$full_filename);
  643. if(isset($this->metafiles[$full_filename])) {
  644. continue;
  645. }
  646. $this->metafiles[$full_filename] = get_metafiles_for_file($this->share, "$this->dir", $file, $this->load_nok_metafiles, $this->quiet, $this->check_symlink);
  647. }
  648. }
  649. closedir($this->dir_handle);
  650. $this->directory_stack = array_unique($this->directory_stack);
  651. }
  652. }
  653. if(count($this->metafiles) > 0) {
  654. break;
  655. }
  656. }
  657. if (!$this->quiet) {
  658. gh_log(DEBUG, 'Found ' . count($this->metafiles) . ' metadata files.');
  659. }
  660. return $this->metafiles;
  661. }
  662. public function valid() {
  663. return count($this->metafiles) > 0;
  664. }
  665. }
  666. function _getopt ( ) {
  667. /* _getopt(): Ver. 1.3 2009/05/30
  668. My page: http://www.ntu.beautifulworldco.com/weblog/?p=526
  669. Usage: _getopt ( [$flag,] $short_option [, $long_option] );
  670. Note that another function split_para() is required, which can be found in the same
  671. page.
  672. _getopt() fully simulates getopt() which is described at
  673. http://us.php.net/manual/en/function.getopt.php , including long options for PHP
  674. version under 5.3.0. (Prior to 5.3.0, long options was only available on few systems)
  675. Besides legacy usage of getopt(), I also added a new option to manipulate your own
  676. argument lists instead of those from command lines. This new option can be a string
  677. or an array such as
  678. $flag = "-f value_f -ab --required 9 --optional=PK --option -v test -k";
  679. or
  680. $flag = array ( "-f", "value_f", "-ab", "--required", "9", "--optional=PK", "--option" );
  681. So there are four ways to work with _getopt(),
  682. 1. _getopt ( $short_option );
  683. it's a legacy usage, same as getopt ( $short_option ).
  684. 2. _getopt ( $short_option, $long_option );
  685. it's a legacy usage, same as getopt ( $short_option, $long_option ).
  686. 3. _getopt ( $flag, $short_option );
  687. use your own argument lists instead of command line arguments.
  688. 4. _getopt ( $flag, $short_option, $long_option );
  689. use your own argument lists instead of command line arguments.
  690. */
  691. if ( func_num_args() == 1 ) {
  692. $flag = $flag_array = $GLOBALS['argv'];
  693. $short_option = func_get_arg ( 0 );
  694. $long_option = array ();
  695. return getopt($short_option);
  696. } else if ( func_num_args() == 2 ) {
  697. if ( is_array ( func_get_arg ( 1 ) ) ) {
  698. $flag = $GLOBALS['argv'];
  699. $short_option = func_get_arg ( 0 );
  700. $long_option = func_get_arg ( 1 );
  701. if (PHP_VERSION_ID >= 50300) { return getopt($short_option, $long_option); }
  702. } else {
  703. $flag = func_get_arg ( 0 );
  704. $short_option = func_get_arg ( 1 );
  705. $long_option = array ();
  706. return getopt($short_option);
  707. }
  708. } else if ( func_num_args() == 3 ) {
  709. $flag = func_get_arg ( 0 );
  710. $short_option = func_get_arg ( 1 );
  711. $long_option = func_get_arg ( 2 );
  712. if (PHP_VERSION_ID >= 50300) { return getopt($short_option, $long_option); }
  713. } else {
  714. exit ( "wrong options\n" );
  715. }
  716. $short_option = trim ( $short_option );
  717. $short_no_value = array();
  718. $short_required_value = array();
  719. $short_optional_value = array();
  720. $long_no_value = array();
  721. $long_required_value = array();
  722. $long_optional_value = array();
  723. $options = array();
  724. for ( $i = 0; $i < strlen ( $short_option ); ) {
  725. if ( $short_option{$i} != ":" ) {
  726. if ( $i == strlen ( $short_option ) - 1 ) {
  727. $short_no_value[] = $short_option{$i};
  728. break;
  729. } else if ( $short_option{$i+1} != ":" ) {
  730. $short_no_value[] = $short_option{$i};
  731. $i++;
  732. continue;
  733. } else if ( $short_option{$i+1} == ":" && $short_option{$i+2} != ":" ) {
  734. $short_required_value[] = $short_option{$i};
  735. $i += 2;
  736. continue;
  737. } else if ( $short_option{$i+1} == ":" && $short_option{$i+2} == ":" ) {
  738. $short_optional_value[] = $short_option{$i};
  739. $i += 3;
  740. continue;
  741. }
  742. } else {
  743. continue;
  744. }
  745. }
  746. foreach ( $long_option as $a ) {
  747. if ( substr( $a, -2 ) == "::" ) {
  748. $long_optional_value[] = substr( $a, 0, -2);
  749. continue;
  750. } else if ( substr( $a, -1 ) == ":" ) {
  751. $long_required_value[] = substr( $a, 0, -1 );
  752. continue;
  753. } else {
  754. $long_no_value[] = $a;
  755. continue;
  756. }
  757. }
  758. if ( is_array ( $flag ) )
  759. $flag_array = $flag;
  760. else {
  761. $flag = "- $flag";
  762. $flag_array = split_para( $flag );
  763. }
  764. for ( $i = 0; $i < count( $flag_array ); ) {
  765. if ( $i >= count ( $flag_array ) )
  766. break;
  767. if ( ! $flag_array[$i] || $flag_array[$i] == "-" ) {
  768. $i++;
  769. continue;
  770. }
  771. if ( $flag_array[$i]{0} != "-" ) {
  772. $i++;
  773. continue;
  774. }
  775. if ( substr( $flag_array[$i], 0, 2 ) == "--" ) {
  776. if (strpos($flag_array[$i], '=') != false) {
  777. list($key, $value) = explode('=', substr($flag_array[$i], 2), 2);
  778. if ( in_array ( $key, $long_required_value ) || in_array ( $key, $long_optional_value ) )
  779. $options[$key][] = $value;
  780. $i++;
  781. continue;
  782. }
  783. if (strpos($flag_array[$i], '=') == false) {
  784. $key = substr( $flag_array[$i], 2 );
  785. if ( in_array( substr( $flag_array[$i], 2 ), $long_required_value ) ) {
  786. $options[$key][] = $flag_array[$i+1];
  787. $i += 2;
  788. continue;
  789. } else if ( in_array( substr( $flag_array[$i], 2 ), $long_optional_value ) ) {
  790. if ( $flag_array[$i+1] != "" && $flag_array[$i+1]{0} != "-" ) {
  791. $options[$key][] = $flag_array[$i+1];
  792. $i += 2;
  793. } else {
  794. $options[$key][] = FALSE;
  795. $i ++;
  796. }
  797. continue;
  798. } else if ( in_array( substr( $flag_array[$i], 2 ), $long_no_value ) ) {
  799. $options[$key][] = FALSE;
  800. $i++;
  801. continue;
  802. } else {
  803. $i++;
  804. continue;
  805. }
  806. }
  807. } else if ( $flag_array[$i]{0} == "-" && $flag_array[$i]{1} != "-" ) {
  808. for ( $j=1; $j < strlen($flag_array[$i]); $j++ ) {
  809. if ( in_array( $flag_array[$i]{$j}, $short_required_value ) || in_array( $flag_array[$i]{$j}, $short_optional_value )) {
  810. if ( $j == strlen($flag_array[$i]) - 1 ) {
  811. if ( in_array( $flag_array[$i]{$j}, $short_required_value ) ) {
  812. $options[$flag_array[$i]{$j}][] = $flag_array[$i+1];
  813. $i += 2;
  814. } else if ( in_array( $flag_array[$i]{$j}, $short_optional_value ) && $flag_array[$i+1] != "" && $flag_array[$i+1]{0} != "-" ) {
  815. $options[$flag_array[$i]{$j}][] = $flag_array[$i+1];
  816. $i += 2;
  817. } else {
  818. $options[$flag_array[$i]{$j}][] = FALSE;
  819. $i ++;
  820. }
  821. $plus_i = 0;
  822. break;
  823. } else {
  824. $options[$flag_array[$i]{$j}][] = substr ( $flag_array[$i], $j + 1 );
  825. $i ++;
  826. $plus_i = 0;
  827. break;
  828. }
  829. } else if ( in_array ( $flag_array[$i]{$j}, $short_no_value ) ) {
  830. $options[$flag_array[$i]{$j}][] = FALSE;
  831. $plus_i = 1;
  832. continue;
  833. } else {
  834. $plus_i = 1;
  835. break;
  836. }
  837. }
  838. $i += $plus_i;
  839. continue;
  840. }
  841. $i++;
  842. continue;
  843. }
  844. foreach ( $options as $key => $value ) {
  845. if ( count ( $value ) == 1 ) {
  846. $options[ $key ] = $value[0];
  847. }
  848. }
  849. return $options;
  850. }
  851. function split_para ( $pattern ) {
  852. /* split_para() version 1.0 2008/08/19
  853. My page: http://www.ntu.beautifulworldco.com/weblog/?p=526
  854. This function is to parse parameters and split them into smaller pieces.
  855. preg_split() does similar thing but in our function, besides "space", we
  856. also take the three symbols " (double quote), '(single quote),
  857. and \ (backslash) into consideration because things in a pair of " or '
  858. should be grouped together.
  859. As an example, this parameter list
  860. -f "test 2" -ab --required "t\"est 1" --optional="te'st 3" --option -v 'test 4'
  861. will be splited into
  862. -f
  863. t"est 2
  864. -ab
  865. --required
  866. test 1
  867. --optional=te'st 3
  868. --option
  869. -v
  870. test 4
  871. see the code below,
  872. $pattern = "-f \"test 2\" -ab --required \"t\\\"est 1\" --optional=\"te'st 3\" --option -v 'test 4'";
  873. $result = split_para( $pattern );
  874. echo "ORIGINAL PATTERN: $pattern\n\n";
  875. var_dump( $result );
  876. */
  877. $begin=0;
  878. $backslash = 0;
  879. $quote = "";
  880. $quote_mark = array();
  881. $result = array();
  882. $pattern = trim ( $pattern );
  883. for ( $end = 0; $end < strlen ( $pattern ) ; ) {
  884. if ( ! in_array ( $pattern{$end}, array ( " ", "\"", "'", "\\" ) ) ) {
  885. $backslash = 0;
  886. $end ++;
  887. continue;
  888. }
  889. if ( $pattern{$end} == "\\" ) {
  890. $backslash++;
  891. $end ++;
  892. continue;
  893. } else if ( $pattern{$end} == "\"" ) {
  894. if ( $backslash % 2 == 1 || $quote == "'" ) {
  895. $backslash = 0;
  896. $end ++;
  897. continue;
  898. }
  899. if ( $quote == "" ) {
  900. $quote_mark[] = $end - $begin;
  901. $quote = "\"";
  902. } else if ( $quote == "\"" ) {
  903. $quote_mark[] = $end - $begin;
  904. $quote = "";
  905. }
  906. $backslash = 0;
  907. $end ++;
  908. continue;
  909. } else if ( $pattern{$end} == "'" ) {
  910. if ( $backslash % 2 == 1 || $quote == "\"" ) {
  911. $backslash = 0;
  912. $end ++;
  913. continue;
  914. }
  915. if ( $quote == "" ) {
  916. $quote_mark[] = $end - $begin;
  917. $quote = "'";
  918. } else if ( $quote == "'" ) {
  919. $quote_mark[] = $end - $begin;
  920. $quote = "";
  921. }
  922. $backslash = 0;
  923. $end ++;
  924. continue;
  925. } else if ( $pattern{$end} == " " ) {
  926. if ( $quote != "" ) {
  927. $backslash = 0;
  928. $end ++;
  929. continue;
  930. } else {
  931. $backslash = 0;
  932. $cand = substr( $pattern, $begin, $end-$begin );
  933. for ( $j = 0; $j < strlen ( $cand ); $j ++ ) {
  934. if ( in_array ( $j, $quote_mark ) )
  935. continue;
  936. $cand1 .= $cand{$j};
  937. }
  938. if ( $cand1 ) {
  939. eval( "\$cand1 = \"$cand1\";" );
  940. $result[] = $cand1;
  941. }
  942. $quote_mark = array();
  943. $cand1 = "";
  944. $end ++;
  945. $begin = $end;
  946. continue;
  947. }
  948. }
  949. }
  950. $cand = substr( $pattern, $begin, $end-$begin );
  951. for ( $j = 0; $j < strlen ( $cand ); $j ++ ) {
  952. if ( in_array ( $j, $quote_mark ) )
  953. continue;
  954. $cand1 .= $cand{$j};
  955. }
  956. eval( "\$cand1 = \"$cand1\";" );
  957. if ( $cand1 )
  958. $result[] = $cand1;
  959. return $result;
  960. }
  961. function kshift(&$arr) {
  962. if (count($arr) == 0) {
  963. return FALSE;
  964. }
  965. foreach ($arr as $k => $v) {
  966. unset($arr[$k]);
  967. break;
  968. }
  969. return array($k, $v);
  970. }
  971. function kshuffle(&$array) {
  972. if (!is_array($array)) { return $array; }
  973. $keys = array_keys($array);
  974. shuffle($keys);
  975. $random = array();
  976. foreach ($keys as $key) {
  977. $random[$key] = $array[$key];
  978. }
  979. $array = $random;
  980. }
  981. class DriveSelection {
  982. var $num_drives_per_draft;
  983. var $selection_algorithm;
  984. var $drives;
  985. var $is_custom;
  986. var $sorted_target_drives;
  987. var $last_resort_sorted_target_drives;
  988. function __construct($num_drives_per_draft, $selection_algorithm, $drives, $is_custom) {
  989. $this->num_drives_per_draft = $num_drives_per_draft;
  990. $this->selection_algorithm = $selection_algorithm;
  991. $this->drives = $drives;
  992. $this->is_custom = $is_custom;
  993. }
  994. function init(&$sorted_target_drives, &$last_resort_sorted_target_drives) {
  995. // Sort by used space (asc) for least_used_space, or by available space (desc) for most_available_space
  996. if ($this->selection_algorithm == 'least_used_space') {
  997. $sorted_target_drives = $sorted_target_drives['used_space'];
  998. $last_resort_sorted_target_drives = $last_resort_sorted_target_drives['used_space'];
  999. asort($sorted_target_drives);
  1000. asort($last_resort_sorted_target_drives);
  1001. } else if ($this->selection_algorithm == 'most_available_space') {
  1002. $sorted_target_drives = $sorted_target_drives['available_space'];
  1003. $last_resort_sorted_target_drives = $last_resort_sorted_target_drives['available_space'];
  1004. arsort($sorted_target_drives);
  1005. arsort($last_resort_sorted_target_drives);
  1006. } else {
  1007. gh_log(CRITICAL, "Unknown drive_selection_algorithm found: " . $this->selection_algorithm);
  1008. }
  1009. // Only keep drives that are in $this->drives
  1010. $this->sorted_target_drives = array();
  1011. foreach ($sorted_target_drives as $sp_drive => $space) {
  1012. if (array_search($sp_drive, $this->drives) !== FALSE) {
  1013. $this->sorted_target_drives[$sp_drive] = $space;
  1014. }
  1015. }
  1016. $this->last_resort_sorted_target_drives = array();
  1017. foreach ($last_resort_sorted_target_drives as $sp_drive => $space) {
  1018. if (array_search($sp_drive, $this->drives) !== FALSE) {
  1019. $this->last_resort_sorted_target_drives[$sp_drive] = $space;
  1020. }
  1021. }
  1022. }
  1023. function draft() {
  1024. $drives = array();
  1025. $drives_last_resort = array();
  1026. while (count($drives)<$this->num_drives_per_draft) {
  1027. $arr = kshift($this->sorted_target_drives);
  1028. if ($arr === FALSE) {
  1029. break;
  1030. }
  1031. list($sp_drive, $space) = $arr;
  1032. if (!is_greyhole_owned_drive($sp_drive)) { continue; }
  1033. $drives[$sp_drive] = $space;
  1034. }
  1035. while (count($drives)+count($drives_last_resort)<$this->num_drives_per_draft) {
  1036. $arr = kshift($this->last_resort_sorted_target_drives);
  1037. if ($arr === FALSE) {
  1038. break;
  1039. }
  1040. list($sp_drive, $space) = $arr;
  1041. if (!is_greyhole_owned_drive($sp_drive)) { continue; }
  1042. $drives_last_resort[$sp_drive] = $space;
  1043. }
  1044. return array($drives, $drives_last_resort);
  1045. }
  1046. static function parse($config_string, $drive_selection_groups) {
  1047. $ds = array();
  1048. if ($config_string == 'least_used_space' || $config_string == 'most_available_space') {
  1049. global $storage_pool_drives;
  1050. $ds[] = new DriveSelection(count($storage_pool_drives), $config_string, $storage_pool_drives, FALSE);
  1051. return $ds;
  1052. }
  1053. if (!preg_match('/forced ?\((.+)\) ?(least_used_space|most_available_space)/i', $config_string, $regs)) {
  1054. gh_log(CRITICAL, "Can't understand the drive_selection_algorithm value: $config_string");
  1055. }
  1056. $selection_algorithm = $regs[2];
  1057. $groups = array_map('trim', explode(',', $regs[1]));
  1058. foreach ($groups as $group) {
  1059. $group = explode(' ', preg_replace('/^([0-9]+)x/', '\\1 ', $group));
  1060. $num_drives = trim($group[0]);
  1061. $group_name = trim($group[1]);
  1062. if (!isset($drive_selection_groups[$group_name])) {
  1063. //gh_log(WARN, "Warning: drive selection group named '$group_name' is undefined.");
  1064. continue;
  1065. }
  1066. if ($num_drives == 'all' || $num_drives > count($drive_selection_groups[$group_name])) {
  1067. $num_drives = count($drive_selection_groups[$group_name]);
  1068. }
  1069. $ds[] = new DriveSelection($num_drives, $selection_algorithm, $drive_selection_groups[$group_name], TRUE);
  1070. }
  1071. return $ds;
  1072. }
  1073. function update() {
  1074. // Make sure num_drives_per_draft and drives have been set, in case storage_pool_drive lines appear after drive_selection_algorithm line(s) in the config file
  1075. if (!$this->is_custom && ($this->selection_algorithm == 'least_used_space' || $this->selection_algorithm == 'most_available_space')) {
  1076. global $storage_pool_drives;
  1077. $this->num_drives_per_draft = count($storage_pool_drives);
  1078. $this->drives = $storage_pool_drives;
  1079. }
  1080. }
  1081. }
  1082. $greyhole_owned_drives = array();
  1083. function is_greyhole_owned_drive($sp_drive) {
  1084. global $going_drive, $df_cache_time, $greyhole_owned_drives;
  1085. if (isset($going_drive) && $sp_drive == $going_drive) {
  1086. return FALSE;
  1087. }
  1088. $is_greyhole_owned_drive = isset($greyhole_owned_drives[$sp_drive]);
  1089. if ($is_greyhole_owned_drive && $greyhole_owned_drives[$sp_drive] < time() - $df_cache_time) {
  1090. unset($greyhole_owned_drives[$sp_drive]);
  1091. $is_greyhole_owned_drive = FALSE;
  1092. }
  1093. if (!$is_greyhole_owned_drive) {
  1094. $drives_definitions = Settings::get('sp_drives_definitions', TRUE);
  1095. if (!$drives_definitions) {
  1096. $drives_definitions = convert_sp_drives_tag_files();
  1097. }
  1098. $drive_uuid = gh_dir_uuid($sp_drive);
  1099. $is_greyhole_owned_drive = @$drives_definitions[$sp_drive] === $drive_uuid && $drive_uuid !== FALSE;
  1100. if (!$is_greyhole_owned_drive) {
  1101. // Maybe this is a remote mount? Those don't have UUIDs, so we use the .greyhole_uses_this technique.
  1102. $is_greyhole_owned_drive = file_exists("$sp_drive/.greyhole_uses_this");
  1103. if ($is_greyhole_owned_drive && isset($drives_definitions[$sp_drive])) {
  1104. // This remote drive was listed in MySQL; it shouldn't be. Let's remove it.
  1105. unset($drives_definitions[$sp_drive]);
  1106. Settings::set('sp_drives_definitions', $drives_definitions);
  1107. }
  1108. }
  1109. if ($is_greyhole_owned_drive) {
  1110. $greyhole_owned_drives[$sp_drive] = time();
  1111. }
  1112. }
  1113. return $is_greyhole_owned_drive;
  1114. }
  1115. // Is it OK for a drive to be gone?
  1116. function gone_ok($sp_drive, $refresh=FALSE) {
  1117. global $gone_ok_drives;
  1118. if ($refresh || !isset($gone_ok_drives)) {
  1119. $gone_ok_drives = get_gone_ok_drives();
  1120. }
  1121. if (isset($gone_ok_drives[$sp_drive])) {
  1122. return TRUE;
  1123. }
  1124. return FALSE;
  1125. }
  1126. function get_gone_ok_drives() {
  1127. global $gone_ok_drives;
  1128. $gone_ok_drives = Settings::get('Gone-OK-Drives', TRUE);
  1129. if (!$gone_ok_drives) {
  1130. $gone_ok_drives = array();
  1131. Settings::set('Gone-OK-Drives', $gone_ok_drives);
  1132. }
  1133. return $gone_ok_drives;
  1134. }
  1135. function mark_gone_ok($sp_drive, $action='add') {
  1136. global $storage_pool_drives;
  1137. if (array_search($sp_drive, $storage_pool_drives) === FALSE) {
  1138. $sp_drive = '/' . trim($sp_drive, '/');
  1139. }
  1140. if (array_search($sp_drive, $storage_pool_drives) === FALSE) {
  1141. return FALSE;
  1142. }
  1143. global $gone_ok_drives;
  1144. $gone_ok_drives = get_gone_ok_drives();
  1145. if ($action == 'add') {
  1146. $gone_ok_drives[$sp_drive] = TRUE;
  1147. } else {
  1148. unset($gone_ok_drives[$sp_drive]);
  1149. }
  1150. Settings::set('Gone-OK-Drives', $gone_ok_drives);
  1151. return TRUE;
  1152. }
  1153. function gone_fscked($sp_drive, $refresh=FALSE) {
  1154. global $fscked_gone_drives;
  1155. if ($refresh || !isset($fscked_gone_drives)) {
  1156. $fscked_gone_drives = get_fsck_gone_drives();
  1157. }
  1158. if (isset($fscked_gone_drives[$sp_drive])) {
  1159. return TRUE;
  1160. }
  1161. return FALSE;
  1162. }
  1163. function get_fsck_gone_drives() {
  1164. global $fscked_gone_drives;
  1165. $fscked_gone_drives = Settings::get('Gone-FSCKed-Drives', TRUE);
  1166. if (!$fscked_gone_drives) {
  1167. $fscked_gone_drives = array();
  1168. Settings::set('Gone-FSCKed-Drives', $fscked_gone_drives);
  1169. }
  1170. return $fscked_gone_drives;
  1171. }
  1172. function mark_gone_drive_fscked($sp_drive, $action='add') {
  1173. global $fscked_gone_drives;
  1174. $fscked_gone_drives = get_fsck_gone_drives();
  1175. if ($action == 'add') {
  1176. $fscked_gone_drives[$sp_drive] = TRUE;
  1177. } else {
  1178. unset($fscked_gone_drives[$sp_drive]);
  1179. }
  1180. Settings::set('Gone-FSCKed-Drives', $fscked_gone_drives);
  1181. }
  1182. function check_storage_pool_drives($skip_fsck=FALSE) {
  1183. global $storage_pool_drives, $email_to, $gone_ok_drives;
  1184. $needs_fsck = FALSE;
  1185. $returned_drives = array();
  1186. $missing_drives = array();
  1187. $i = 0; $j = 0;
  1188. foreach ($storage_pool_drives as $sp_drive) {
  1189. if (!is_greyhole_owned_drive($sp_drive) && !gone_fscked($sp_drive, $i++ == 0) && !file_exists("$sp_drive/.greyhole_used_this")) {
  1190. if($needs_fsck !== 2){
  1191. $needs_fsck = 1;
  1192. }
  1193. mark_gone_drive_fscked($sp_drive);
  1194. $missing_drives[] = $sp_drive;
  1195. gh_log(WARN, "Warning! It seems the partition UUID of $sp_drive changed. This probably means this mount is currently unmounted, or that you replaced this drive and didn't use 'greyhole --replace'. Because of that, Greyhole will NOT use this drive at this time.");
  1196. gh_log(DEBUG, "Email sent for gone drive: $sp_drive");
  1197. $gone_ok_drives[$sp_drive] = TRUE; // The upcoming fsck should not recreate missing copies just yet
  1198. } else if ((gone_ok($sp_drive, $j++ == 0) || gone_fscked($sp_drive, $i++ == 0)) && is_greyhole_owned_drive($sp_drive)) {
  1199. // $sp_drive is now back
  1200. $needs_fsck = 2;
  1201. $returned_drives[] = $sp_drive;
  1202. gh_log(DEBUG, "Email sent for revived drive: $sp_drive");
  1203. mark_gone_ok($sp_drive, 'remove');
  1204. mark_gone_drive_fscked($sp_drive, 'remove');
  1205. $i = 0; $j = 0;
  1206. }
  1207. }
  1208. if(count($returned_drives) > 0) {
  1209. $body = "This is an automated email from Greyhole.\n\nOne (or more) of your storage pool drives came back:\n";
  1210. foreach ($returned_drives as $sp_drive) {
  1211. $body .= "$sp_drive was missing; it's now available again.\n";
  1212. }
  1213. if (!$skip_fsck) {
  1214. $body .= "\nA fsck will now start, to fix the symlinks found in your shares, when possible.\nYou'll receive a report email once that fsck run completes.\n";
  1215. }
  1216. $drive_string = join(", ", $returned_drives);
  1217. $subject = "Storage pool drive now online on " . exec ('hostname') . ": ";
  1218. $subject = $subject . $drive_string;
  1219. if (strlen($subject) > 255) {
  1220. $subject = substr($subject, 0, 255);
  1221. }
  1222. mail($email_to, $subject, $body);
  1223. }
  1224. if(count($missing_drives) > 0) {
  1225. $body = "This is an automated email from Greyhole.\n\nOne (or more) of your storage pool drives has disappeared:\n";
  1226. $drives_definitions = Settings::get('sp_drives_definitions', TRUE);
  1227. foreach ($missing_drives as $sp_drive) {
  1228. if (!is_dir($sp_drive)) {
  1229. $body .= "$sp_drive: directory doesn't exists\n";
  1230. } else {
  1231. $current_uuid = gh_dir_uuid($sp_drive);
  1232. if (empty($current_uuid)) {
  1233. $current_uuid = 'N/A';
  1234. }
  1235. $body .= "$sp_drive: expected partition UUID: " . $drives_definitions[$sp_drive] . "; current partition UUID: $current_uuid\n";
  1236. }
  1237. }
  1238. $sp_drive = $missing_drives[0];
  1239. $body .= "\nThis either means this mount is currently unmounted, or you forgot to use 'greyhole --replace' when you changed this drive.\n\n";
  1240. $body .= "Here are your options:\n\n";
  1241. $body .= "- If you forgot to use 'greyhole --replace', you should do so now. Until you do, this drive will not be part of your storage pool.\n\n";
  1242. $body .= "- If the drive is gone, you should either re-mount it manually (if possible), or remove it from your storage pool. To do so, use the following command:\n greyhole --gone=" . escapeshellarg($sp_drive) . "\n Note that the above command is REQUIRED for Greyhole to re-create missing file copies before the next fsck runs. Until either happens, missing file copies WILL NOT be re-created on other drives.\n\n";
  1243. $body .= "- If you know this drive will come back soon, and do NOT want Greyhole to re-create missing file copies for this drive until it reappears, you should execute this command:\n greyhole --wait-for=" . escapeshellarg($sp_drive) . "\n\n";
  1244. if (!$skip_fsck) {
  1245. $body .= "A fsck will now start, to fix the symlinks found in your shares, when possible.\nYou'll receive a report email once that fsck run completes.\n";
  1246. }
  1247. $subject = "Missing storage pool drives on " . exec('hostname') . ": ";
  1248. $drive_string = join(",",$missing_drives);
  1249. $subject = $subject . $drive_string;
  1250. if (strlen($subject) > 255) {
  1251. $subject = substr($subject, 0, 255);
  1252. }
  1253. mail($email_to, $subject, $body);
  1254. }
  1255. if ($needs_fsck !== FALSE) {
  1256. set_metastore_backup();
  1257. get_metastores(FALSE); // FALSE => Resets the metastores cache
  1258. clearstatcache();
  1259. if (!$skip_fsck) {
  1260. global $shares_options;
  1261. initialize_fsck_report('All shares');
  1262. if($needs_fsck === 2) {
  1263. foreach ($returned_drives as $drive) {
  1264. $metastores = get_metastores_from_storage_volume($drive);
  1265. gh_log(INFO, "Starting fsck for metadata store on $drive which came back online.");
  1266. foreach($metastores as $metastore) {
  1267. foreach($shares_options as $share_name => $share_options) {
  1268. gh_fsck_metastore($metastore,"/$share_name", $share_name);
  1269. }
  1270. }
  1271. gh_log(INFO, "fsck for returning drive $drive's metadata store completed.");
  1272. }
  1273. gh_log(INFO, "Starting fsck for all shares - caused by missing drive that came back online.");
  1274. }else{
  1275. gh_log(INFO, "Starting fsck for all shares - caused by missing drive. Will just recreate symlinks to existing copies when possible; won't create new copies just yet.");
  1276. fix_all_symlinks();
  1277. }
  1278. schedule_fsck_all_shares(array('email'));
  1279. gh_log(INFO, " fsck for all shares scheduled.");
  1280. }
  1281. // Refresh $gone_ok_drives to it's real value (from the DB)
  1282. get_gone_ok_drives();
  1283. }
  1284. }
  1285. class FSCKLogFile {
  1286. const PATH = '/usr/share/greyhole';
  1287. private $path;
  1288. private $filename;
  1289. private $lastEmailSentTime = 0;
  1290. public function __construct($filename, $path=self::PATH) {
  1291. $this->filename = $filename;
  1292. $this->path = $path;
  1293. }
  1294. public function emailAsRequired() {
  1295. $logfile = "$this->path/$this->filename";
  1296. if (!file_exists($logfile)) { return; }
  1297. $last_mod_date = filemtime($logfile);
  1298. if ($last_mod_date > $this->getLastEmailSentTime()) {
  1299. global $email_to;
  1300. gh_log(WARN, "Sending $logfile by email to $email_to");
  1301. mail($email_to, $this->getSubject(), $this->getBody());
  1302. $this->lastEmailSentTime = $last_mod_date;
  1303. Settings::set("last_email_$this->filename", $this->lastEmailSentTime);
  1304. }
  1305. }
  1306. private function getBody() {
  1307. $logfile = "$this->path/$this->filename";
  1308. if ($this->filename == 'fsck_checksums.log') {
  1309. return file_get_contents($logfile) . "\nNote: You should manually delete the $logfile file once you're done with it.";
  1310. } else if ($this->filename == 'fsck_files.log') {
  1311. global $fsck_report;
  1312. $fsck_report = unserialize(file_get_contents($logfile));
  1313. unlink($logfile);
  1314. return get_fsck_report() . "\nNote: This report is a complement to the last report you've received. It details possible errors with files for which the fsck was postponed.";
  1315. } else {
  1316. return '[empty]';
  1317. }
  1318. }
  1319. private function getSubject() {
  1320. if ($this->filename == 'fsck_checksums.log') {
  1321. return 'Mismatched checksums in Greyhole file copies';
  1322. } else if ($this->filename == 'fsck_files.log') {
  1323. return 'fsck_files of Greyhole shares on ' . exec('hostname');
  1324. } else {
  1325. return 'Unknown FSCK report';
  1326. }
  1327. }
  1328. private function getLastEmailSentTime() {
  1329. if ($this->lastEmailSentTime == 0) {
  1330. $setting = Settings::get("last_email_$this->filename");
  1331. if ($setting) {
  1332. $this->lastEmailSentTime = (int) $setting;
  1333. }
  1334. }
  1335. return $this->lastEmailSentTime;
  1336. }
  1337. public static function loadFSCKReport($what) {
  1338. $logfile = self::PATH . '/fsck_files.log';
  1339. if (file_exists($logfile)) {
  1340. global $fsck_report;
  1341. $fsck_report = unserialize(file_get_contents($log