#!/usr/bin/greyhole-php
<?php
/*
Copyright 2009-2020 Guillaume Boudreau, Andrew Hopkinson
This file is part of Greyhole.
Greyhole is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Greyhole is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Greyhole.  If not, see <http://www.gnu.org/licenses/>.
*/
define('CONFIG_DAEMON_NICENESS', 'daemon_niceness');
define('CONFIG_LOG_LEVEL', 'log_level');
define('CONFIG_DELETE_MOVES_TO_TRASH', 'delete_moves_to_trash');
define('CONFIG_MODIFIED_MOVES_TO_TRASH', 'modified_moves_to_trash');
define('CONFIG_LOG_MEMORY_USAGE', 'log_memory_usage');
define('CONFIG_CHECK_FOR_OPEN_FILES', 'check_for_open_files');
define('CONFIG_ALLOW_MULTIPLE_SP_PER_DRIVE', 'allow_multiple_sp_per_device');
define('CONFIG_STORAGE_POOL_DRIVE', 'storage_pool_drive');
define('CONFIG_MIN_FREE_SPACE_POOL_DRIVE', 'min_free_space_pool_drive');
define('CONFIG_STICKY_FILES', 'sticky_files');
define('CONFIG_STICK_INTO', 'stick_into');
define('CONFIG_FROZEN_DIRECTORY', 'frozen_directory');
define('CONFIG_MEMORY_LIMIT', 'memory_limit');
define('CONFIG_TIMEZONE', 'timezone');
define('CONFIG_DRIVE_SELECTION_GROUPS', 'drive_selection_groups');
define('CONFIG_DRIVE_SELECTION_ALGORITHM', 'drive_selection_algorithm');
define('CONFIG_IGNORED_FILES', 'ignored_files');
define('CONFIG_IGNORED_FOLDERS', 'ignored_folders');
define('CONFIG_NUM_COPIES', 'num_copies');
define('CONFIG_LANDING_ZONE', 'landing_zone');
define('CONFIG_MAX_QUEUED_TASKS', 'max_queued_tasks');
define('CONFIG_EXECUTED_TASKS_RETENTION', 'executed_tasks_retention');
define('CONFIG_GREYHOLE_LOG_FILE', 'greyhole_log_file');
define('CONFIG_GREYHOLE_ERROR_LOG_FILE', 'greyhole_error_log_file');
define('CONFIG_EMAIL_TO', 'email_to');
define('CONFIG_DF_CACHE_TIME', 'df_cache_time');
define('CONFIG_DB_HOST', 'db_host');
define('CONFIG_DB_USER', 'db_user');
define('CONFIG_DB_PASS', 'db_pass');
define('CONFIG_DB_NAME', 'db_name');
define('CONFIG_METASTORE_BACKUPS', 'metastore_backups');
define('CONFIG_TRASH_SHARE', '===trash_share===');
define('CONFIG_HOOK', 'hook');
define('CONFIG_CHECK_SP_SCHEDULE', 'check_storage_pool_schedule');
define('CONFIG_CALCULATE_MD5_DURING_COPY', 'calculate_md5');
define('CONFIG_PARALLEL_COPYING', 'parallel_copying');
function recursive_include_parser($file) {
    $regex = '/^[ \t]*include[ \t]*=[ \t]*([^#\r\n]+)/im';
    $ok_to_execute = FALSE;
    if (is_array($file) && count($file) > 1) {
        $file = $file[1];
    }
    $file = trim($file);
    if (file_exists($file)) {
        if (is_executable($file)) {
            $perms = fileperms($file);
            $ok_to_execute = !($perms & 0x0080) || fileowner($file) === 0;
            $ok_to_execute &= !($perms & 0x0010) || filegroup($file) === 0;
            $ok_to_execute &= !($perms & 0x0002);
            if (!$ok_to_execute) {
                Log::warn("Config file '{$file}' is executable but file permissions are insecure, only the file's contents will be included.", Log::EVENT_CODE_CONFIG_INCLUDE_INSECURE_PERMISSIONS);
            }
        }
        $contents = $ok_to_execute ? shell_exec(escapeshellcmd($file)) : file_get_contents($file);
        return preg_replace_callback($regex, 'recursive_include_parser', $contents);
    } else {
        return false;
    }
}
final class ConfigHelper {
    static $df_command;
    public static $config_file = '/etc/greyhole.conf';
    public static $smb_config_file = '/etc/samba/smb.conf';
    public static $trash_share_names = array('Greyhole Attic', 'Greyhole Trash', 'Greyhole Recycle Bin');
    static $deprecated_options = array(
        'delete_moves_to_attic' => CONFIG_DELETE_MOVES_TO_TRASH,
        'storage_pool_directory' => CONFIG_STORAGE_POOL_DRIVE,
        'dir_selection_groups' => CONFIG_DRIVE_SELECTION_GROUPS,
        'dir_selection_algorithm' => CONFIG_DRIVE_SELECTION_ALGORITHM,
    );
    static $config_options = array(
        'bool' => array(
            CONFIG_DELETE_MOVES_TO_TRASH,
            CONFIG_MODIFIED_MOVES_TO_TRASH,
            CONFIG_LOG_MEMORY_USAGE,
            CONFIG_CHECK_FOR_OPEN_FILES,
            CONFIG_ALLOW_MULTIPLE_SP_PER_DRIVE,
            CONFIG_CALCULATE_MD5_DURING_COPY,
            CONFIG_PARALLEL_COPYING,
        ),
        'number' => array(
            CONFIG_MAX_QUEUED_TASKS,
            CONFIG_EXECUTED_TASKS_RETENTION,
            CONFIG_DF_CACHE_TIME,
        ),
        'string' => array(
            CONFIG_DB_HOST,
            CONFIG_DB_USER,
            CONFIG_DB_PASS,
            CONFIG_DB_NAME,
            CONFIG_EMAIL_TO,
            CONFIG_GREYHOLE_LOG_FILE,
            CONFIG_GREYHOLE_ERROR_LOG_FILE,
            CONFIG_TIMEZONE,
            CONFIG_MEMORY_LIMIT,
            CONFIG_CHECK_SP_SCHEDULE
        ),
    );
    public static function removeShare($share) {
        $conf_file = escapeshellarg(self::$config_file);
        $tmp_file = escapeshellarg(self::$config_file . '.tmp');
        exec("/bin/sed 's/^.*num_copies\[".$share."\].*$//' $conf_file >$tmp_file && cat $tmp_file >$conf_file");
    }
    public static function removeStoragePoolDrive($sp_drive) {
        $escaped_drive = str_replace('/', '\/', $sp_drive);
        $conf_file = escapeshellarg(self::$config_file);
        $tmp_file = escapeshellarg(self::$config_file . '.tmp');
        exec("/bin/sed 's/^.*storage_pool_drive.*$escaped_drive.*$//' $conf_file >$tmp_file && cat $tmp_file >$conf_file");
    }
    public static function randomStoragePoolDrive() {
        $storage_pool_drives = (array) Config::storagePoolDrives();
        return $storage_pool_drives[array_rand($storage_pool_drives)];
    }
    public static function parse() {
        if (!ini_get('date.timezone')) {
            date_default_timezone_set('UTC');
        }
        $config_text = recursive_include_parser(self::$config_file);
        global $parsing_drive_selection_groups;
        foreach (explode("\n", $config_text) as $line) {
            if (preg_match("/^[ \t]*([^=\t]+)[ \t]*=[ \t]*([^#]+)/", $line, $regs)) {
                $name = trim($regs[1]);
                $value = trim($regs[2]);
                self::parse_line($name, $value);
            } else if ($parsing_drive_selection_groups !== FALSE) {
                $value = trim($line);
                if (strlen($value) == 0 || $value[0] == '#') {
                    continue;
                }
                if (preg_match("/(.+):(.*)/", $value, $regs)) {
                    $group_name = trim($regs[1]);
                    $drives = array_map('trim', explode(',', $regs[2]));
                    if (is_string($parsing_drive_selection_groups)) {
                        $share = $parsing_drive_selection_groups;
                        SharesConfig::add($share, CONFIG_DRIVE_SELECTION_GROUPS, $drives, $group_name);
                    } else {
                        Config::add(CONFIG_DRIVE_SELECTION_GROUPS, $drives, $group_name);
                    }
                }
            }
        }
        return self::init();
    }
    private static function parse_line($name, $value) {
        if ($name[0] == '#') {
            return;
        }
        self::normalize_name($name);
        global $parsing_drive_selection_groups;
        $parsing_drive_selection_groups = FALSE;
        if (self::parse_line_bool($name, $value)) return;
        if (self::parse_line_number($name, $value)) return;
        if (self::parse_line_string($name, $value)) return;
        if (self::parse_line_log($name, $value)) return;
        if (self::parse_line_pool_drive($name, $value)) return;
        if (self::parse_line_drive_selection($name, $value)) return;
        if (self::parse_line_sticky($name, $value)) return;
        if (self::parse_line_frozen($name, $value)) return;
        if (self::parse_line_ignore($name, $value)) return;
        if (self::parse_line_share_option($name, $value)) return;
        if (self::parse_line_hook($name, $value)) return;
        if (is_numeric($value)) {
            $value = (int) $value;
        }
        Config::set($name, $value);
    }
    private static function normalize_name(&$name) {
        foreach (self::$deprecated_options as $old_name => $new_name) {
            if (string_contains($name, $old_name)) {
                $fixed_name = str_replace($old_name, $new_name, $name);
                Log::warn("Deprecated option found in greyhole.conf: $name. You should change that to: $fixed_name", Log::EVENT_CODE_CONFIG_DEPRECATED_OPTION);
                $name = $fixed_name;
            }
        }
    }
    private static function parse_line_bool($name, $value) {
        if (array_contains(self::$config_options['bool'], $name)) {
            $bool = trim($value) === '1' || mb_stripos($value, 'yes') !== FALSE || mb_stripos($value, 'true') !== FALSE;
            Config::set($name, $bool);
            return TRUE;
        }
        return FALSE;
    }
    private static function parse_line_number($name, $value) {
        if (array_contains(self::$config_options['number'], $name)) {
            if (is_numeric($value)) {
                $value = (int) $value;
            }
            Config::set($name, $value);
            return TRUE;
        }
        return FALSE;
    }
    private static function parse_line_string($name, $value) {
        if (array_contains(self::$config_options['string'], $name)) {
            Config::set($name, $value);
            return TRUE;
        }
        return FALSE;
    }
    private static function parse_line_log($name, $value) {
        if ($name == CONFIG_LOG_LEVEL) {
            self::assert(defined("Log::$value"), "Invalid value for log_level: '$value'", Log::EVENT_CODE_CONFIG_INVALID_VALUE);
            Config::set(CONFIG_LOG_LEVEL . "_raw", $value);
            Config::set(CONFIG_LOG_LEVEL, constant("Log::$value"));
            return TRUE;
        }
        return FALSE;
    }
    private static function parse_line_pool_drive($name, $value) {
        if ($name == CONFIG_STORAGE_POOL_DRIVE) {
            if (preg_match("/(.*) ?, ?min_free ?: ?([0-9]+) ?([gmk])b?/i", $value, $regs)) {
                $sp_drive = '/' . trim(trim($regs[1]), '/');
                Config::add(CONFIG_STORAGE_POOL_DRIVE, $sp_drive);
                $units = strtolower($regs[3]);
                if ($units == 'g') {
                    $value = (float) trim($regs[2]) * 1024.0 * 1024.0;
                } else if ($units == 'm') {
                    $value = (float) trim($regs[2]) * 1024.0;
                } else if ($units == 'k') {
                    $value = (float) trim($regs[2]);
                }
                Config::add(CONFIG_MIN_FREE_SPACE_POOL_DRIVE, $value, $sp_drive);
            } else {
                Log::warn("Warning! Unable to parse " . CONFIG_STORAGE_POOL_DRIVE . " line from config file. Value = $value", Log::EVENT_CODE_CONFIG_UNPARSEABLE_LINE);
            }
            return TRUE;
        }
        return FALSE;
    }
    private static function parse_line_sticky($name, $value) {
        if ($name == CONFIG_STICKY_FILES) {
            global $last_sticky_files_dir;
            $last_sticky_files_dir = trim($value, '/');
            Config::add(CONFIG_STICKY_FILES, array(), $last_sticky_files_dir);
            return TRUE;
        }
        if ($name == CONFIG_STICK_INTO) {
            global $last_sticky_files_dir;
            $sticky_files = Config::get(CONFIG_STICKY_FILES);
            $sticky_files[$last_sticky_files_dir][] = '/' . trim($value, '/');
            Config::set(CONFIG_STICKY_FILES, $sticky_files);
            return TRUE;
        }
        return FALSE;
    }
    private static function parse_line_frozen($name, $value) {
        if ($name == CONFIG_FROZEN_DIRECTORY) {
            Config::add(CONFIG_FROZEN_DIRECTORY, trim($value, '/'));
            return TRUE;
        }
        return FALSE;
    }
    private static function parse_line_drive_selection($name, $value) {
        if ($name == CONFIG_DRIVE_SELECTION_GROUPS) {
            if (preg_match("/(.+):(.*)/", $value, $regs)) {
                $group_name = trim($regs[1]);
                $group_definition = array_map('trim', explode(',', $regs[2]));
                Config::add(CONFIG_DRIVE_SELECTION_GROUPS, $group_definition, $group_name);
                global $parsing_drive_selection_groups;
                $parsing_drive_selection_groups = TRUE;
            }
            return TRUE;
        }
        if ($name == CONFIG_DRIVE_SELECTION_ALGORITHM) {
            Config::set(CONFIG_DRIVE_SELECTION_ALGORITHM, PoolDriveSelector::parse($value, Config::get(CONFIG_DRIVE_SELECTION_GROUPS)));
            return TRUE;
        }
        return FALSE;
    }
    private static function parse_line_ignore($name, $value) {
        if ($name == CONFIG_IGNORED_FILES) {
            Config::add(CONFIG_IGNORED_FILES, $value);
            return TRUE;
        }
        if ($name == CONFIG_IGNORED_FOLDERS) {
            Config::add(CONFIG_IGNORED_FOLDERS, $value);
            return TRUE;
        }
        return FALSE;
    }
    private static function parse_line_share_option($name, $value) {
        if (!string_starts_with($name, [CONFIG_NUM_COPIES, CONFIG_DELETE_MOVES_TO_TRASH, CONFIG_MODIFIED_MOVES_TO_TRASH, CONFIG_DRIVE_SELECTION_GROUPS, CONFIG_DRIVE_SELECTION_ALGORITHM])) {
            return FALSE;
        }
        if (!preg_match('/^(.*)\[\s*(.*)\s*]$/', $name, $matches)) {
            error_log("Error parsing config file; can't find share name in $name");
            return FALSE;
        }
        $name = trim($matches[1]);
        $share = trim($matches[2]);
        switch ($name) {
        case CONFIG_NUM_COPIES:
            SharesConfig::set($share, $name . '_raw', $value);
            if (mb_stripos($value, 'max') === 0) {
                $value = 9999;
            } else {
                $value = (int) $value;
            }
            SharesConfig::set($share, $name, $value);
            break;
        case CONFIG_DELETE_MOVES_TO_TRASH:
        case CONFIG_MODIFIED_MOVES_TO_TRASH:
            $value = strtolower($value);
            $bool = $value === '1' || mb_stripos($value, 'yes') !== FALSE || mb_stripos($value, 'true') !== FALSE;
            SharesConfig::set($share, $name, $bool);
            break;
        case CONFIG_DRIVE_SELECTION_GROUPS:
            if (preg_match("/(.+):(.+)/", $value, $regs)) {
                $group_name = trim($regs[1]);
                $group_definition = array_map('trim', explode(',', $regs[2]));
                SharesConfig::add($share, CONFIG_DRIVE_SELECTION_GROUPS, $group_definition, $group_name);
                global $parsing_drive_selection_groups;
                $parsing_drive_selection_groups = $share;
            }
            break;
        case CONFIG_DRIVE_SELECTION_ALGORITHM:
            if (SharesConfig::get($share, CONFIG_DRIVE_SELECTION_GROUPS) === FALSE) {
                SharesConfig::set($share, CONFIG_DRIVE_SELECTION_GROUPS, Config::get(CONFIG_DRIVE_SELECTION_GROUPS));
            }
            SharesConfig::set($share, CONFIG_DRIVE_SELECTION_ALGORITHM, PoolDriveSelector::parse($value, SharesConfig::get($share, CONFIG_DRIVE_SELECTION_GROUPS)));
            break;
        }
        return TRUE;
    }
    private static function parse_line_hook($name, $value) {
        if (string_starts_with($name, CONFIG_HOOK)) {
            if (!preg_match('/hook\[([^]]+)]/', $name, $re)) {
                Log::warn("Can't parse the following config line: $name; ignoring.", Log::EVENT_CODE_CONFIG_UNPARSEABLE_LINE);
                return TRUE;
            }
            if (!is_executable($value)) {
                Log::warn("Hook script $value is not executable; ignoring.", Log::EVENT_CODE_CONFIG_HOOK_SCRIPT_NOT_EXECUTABLE);
                return TRUE;
            }
            $events = explode('|', $re[1]);
            foreach ($events as $event) {
                Hook::add($event, $value);
            }
            return TRUE;
        }
        return FALSE;
    }
    private static function init() {
        Log::setLevel(Config::get(CONFIG_LOG_LEVEL));
        self::$df_command = "df -k";
        foreach (Config::storagePoolDrives() as $sp_drive) {
            self::$df_command .= " " . escapeshellarg($sp_drive);
        }
        self::$df_command .= " 2>&1 | grep '%' | grep -v \"^df: .*: No such file or directory$\"";
        exec('testparm -s ' . escapeshellarg(self::$smb_config_file) . ' 2> /dev/null', $config_text);
        if (empty($config_text)) {
            Log::critical("Failed to list Samba configuration using 'testparm -s ".self::$smb_config_file."'.", Log::EVENT_CODE_CONFIG_TESTPARM_FAILED);
        }
        foreach ($config_text as $line) {
            $line = trim($line);
            if (mb_strlen($line) == 0) { continue; }
            if ($line[0] == '[' && preg_match('/\[([^]]+)]/', $line, $regs)) {
                $share_name = $regs[1];
            }
            if (isset($share_name) && !SharesConfig::exists($share_name) && !array_contains(self::$trash_share_names, $share_name)) {
                continue;
            }
            if (isset($share_name) && preg_match('/^\s*path[ \t]*=[ \t]*(.+)$/i', $line, $regs)) {
                SharesConfig::set($share_name, CONFIG_LANDING_ZONE, '/' . trim($regs[1], '/"'));
                SharesConfig::set($share_name, 'name', $share_name);
            }
        }
        $drive_selection_algorithm = Config::get(CONFIG_DRIVE_SELECTION_ALGORITHM);
        if (!empty($drive_selection_algorithm)) {
            foreach ($drive_selection_algorithm as $ds) {
                $ds->update();
            }
        } else {
            $drive_selection_algorithm = PoolDriveSelector::parse('most_available_space', null);
        }
        Config::set(CONFIG_DRIVE_SELECTION_ALGORITHM, $drive_selection_algorithm);
        if (!Config::exists(CONFIG_MODIFIED_MOVES_TO_TRASH)) {
            Config::set(CONFIG_MODIFIED_MOVES_TO_TRASH, Config::get(CONFIG_DELETE_MOVES_TO_TRASH));
        }
        foreach (SharesConfig::getShares() as $share_name => $share_options) {
            if (array_contains(self::$trash_share_names, $share_name)) {
                SharesConfig::set(CONFIG_TRASH_SHARE, 'name', $share_name);
                SharesConfig::set(CONFIG_TRASH_SHARE, CONFIG_LANDING_ZONE, SharesConfig::get($share_name, CONFIG_LANDING_ZONE));
                SharesConfig::removeShare($share_name);
                continue;
            }
            if ($share_options[CONFIG_NUM_COPIES] > count(Config::storagePoolDrives())) {
                SharesConfig::set($share_name, CONFIG_NUM_COPIES, count(Config::storagePoolDrives()));
            }
            if (!isset($share_options[CONFIG_LANDING_ZONE])) {
                Log::warn("Found a share ($share_name) defined in " . self::$config_file . " with no path in " . self::$smb_config_file . ". Either add this share in " . self::$smb_config_file . ", or remove it from " . self::$config_file . ", then restart Greyhole.", Log::EVENT_CODE_CONFIG_SHARE_MISSING_FROM_SMB_CONF);
                return FALSE;
            }
            if (!isset($share_options[CONFIG_DELETE_MOVES_TO_TRASH])) {
                SharesConfig::set($share_name, CONFIG_DELETE_MOVES_TO_TRASH, Config::get(CONFIG_DELETE_MOVES_TO_TRASH));
            }
            if (!isset($share_options[CONFIG_MODIFIED_MOVES_TO_TRASH])) {
                SharesConfig::set($share_name, CONFIG_MODIFIED_MOVES_TO_TRASH, SharesConfig::get($share_name, CONFIG_DELETE_MOVES_TO_TRASH));
            }
            if (isset($share_options[CONFIG_DRIVE_SELECTION_ALGORITHM])) {
                foreach ($share_options[CONFIG_DRIVE_SELECTION_ALGORITHM] as $ds) {
                    $ds->update();
                }
            } else {
                SharesConfig::set($share_name, CONFIG_DRIVE_SELECTION_ALGORITHM, $drive_selection_algorithm);
            }
            if (isset($share_options[CONFIG_DRIVE_SELECTION_GROUPS])) {
                SharesConfig::remove($share_name, CONFIG_DRIVE_SELECTION_GROUPS);
            }
            foreach (Config::storagePoolDrives() as $sp_drive) {
                if (string_starts_with($share_options[CONFIG_LANDING_ZONE], $sp_drive)) {
                    Log::critical("Found a share ($share_name), with path " . $share_options[CONFIG_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.", Log::EVENT_CODE_CONFIG_LZ_INSIDE_STORAGE_POOL);
                }
                if (string_starts_with($sp_drive, $share_options[CONFIG_LANDING_ZONE])) {
                    Log::critical("Found a storage pool drive ($sp_drive), which is INSIDE a share landing zone (" . $share_options[CONFIG_LANDING_ZONE] . "), for share $share_name. Storage pool drives should never be inside a directory that you use as a share landing zone ('path' in smb.conf).\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.", Log::EVENT_CODE_CONFIG_STORAGE_POOL_INSIDE_LZ);
                }
            }
        }
        foreach (Config::storagePoolDrives() as $sp_drive) {
            $found = FALSE;
            foreach (SharesConfig::getShares() as $share_name => $share_options) {
                foreach ($share_options[CONFIG_DRIVE_SELECTION_ALGORITHM] as $ds) {
                    if (array_contains($ds->drives, $sp_drive)) {
                        $found = TRUE;
                    }
                }
            }
            if (!$found) {
                Log::warn("The storage pool drive '$sp_drive' is not part of any drive_selection_algorithm definition, and will thus never be used to receive any files.", Log::EVENT_CODE_CONFIG_STORAGE_POOL_DRIVE_NOT_IN_DRIVE_SELECTION_ALGO);
            }
        }
        $memory_limit = Config::get(CONFIG_MEMORY_LIMIT);
        ini_set('memory_limit', $memory_limit);
        if (preg_match('/G$/i',$memory_limit)) {
            $memory_limit = preg_replace('/G$/i','',$memory_limit);
            $memory_limit = $memory_limit * 1024 * 1024 * 1024;
        } else if (preg_match('/M$/i',$memory_limit)) {
            $memory_limit = preg_replace('/M$/i','',$memory_limit);
            $memory_limit = $memory_limit * 1024 * 1024;
        } else if (preg_match('/K$/i',$memory_limit)) {
            $memory_limit = preg_replace('/K$/i','',$memory_limit);
            $memory_limit = $memory_limit * 1024;
        }
        Config::set(CONFIG_MEMORY_LIMIT, $memory_limit);
        $tz = Config::get(CONFIG_TIMEZONE);
        if (empty($tz)) {
            $tz = @date_default_timezone_get();
        }
        date_default_timezone_set($tz);
        $db_options = array(
            'engine' => 'mysql',
            'schema' => "/usr/share/greyhole/schema-mysql.sql",
            'host' => Config::get(CONFIG_DB_HOST),
            'user' => Config::get(CONFIG_DB_USER),
            'pass' => Config::get(CONFIG_DB_PASS),
            'name' => Config::get(CONFIG_DB_NAME),
        );
        DB::setOptions($db_options);
        if (strtolower(Config::get(CONFIG_GREYHOLE_LOG_FILE)) == 'syslog') {
            openlog("Greyhole", LOG_PID, LOG_USER);
        }
        if (count(Config::storagePoolDrives()) == 0) {
            Log::error("You have no '" . CONFIG_STORAGE_POOL_DRIVE . "' defined. Greyhole can't run.", Log::EVENT_CODE_CONFIG_NO_STORAGE_POOL);
            return FALSE;
        }
        return TRUE;
    }
    private static function assert($check, $error_message, $event_code) {
        if ($check === FALSE) {
            Log::critical($error_message, $event_code);
        }
    }
    public static function test() {
        while (!ConfigHelper::parse()) {
            if (SystemHelper::is_amahi() && Log::actionIs(ACTION_DAEMON)) {
                sleep(600); // 10 minutes
            } else {
                Log::critical("Config file parsing failed. Exiting.", Log::EVENT_CODE_CONFIG_FILE_PARSING_FAILED);
            }
        }
    }
}
final class Config {
    public static $config = array(
        CONFIG_LOG_LEVEL                   => Log::DEBUG,
        CONFIG_DELETE_MOVES_TO_TRASH       => TRUE,
        CONFIG_LOG_MEMORY_USAGE            => FALSE,
        CONFIG_CALCULATE_MD5_DURING_COPY   => TRUE,
        CONFIG_PARALLEL_COPYING            => TRUE,
        CONFIG_CHECK_FOR_OPEN_FILES        => TRUE,
        CONFIG_ALLOW_MULTIPLE_SP_PER_DRIVE => FALSE,
        CONFIG_STORAGE_POOL_DRIVE          => array(),
        CONFIG_MIN_FREE_SPACE_POOL_DRIVE   => array(),
        CONFIG_STICKY_FILES                => array(),
        CONFIG_FROZEN_DIRECTORY            => array(),
        CONFIG_MEMORY_LIMIT                => '512M',
        CONFIG_TIMEZONE                    => FALSE,
        CONFIG_DRIVE_SELECTION_GROUPS      => array(),
        CONFIG_IGNORED_FILES               => array(),
        CONFIG_IGNORED_FOLDERS             => array(),
        CONFIG_MAX_QUEUED_TASKS            => 10000000,
        CONFIG_EXECUTED_TASKS_RETENTION    => 60,
        CONFIG_GREYHOLE_LOG_FILE           => '/var/log/greyhole.log',
        CONFIG_GREYHOLE_ERROR_LOG_FILE     => FALSE,
        CONFIG_EMAIL_TO                    => 'root',
        CONFIG_DF_CACHE_TIME               => 15,
        CONFIG_CHECK_SP_SCHEDULE           => NULL
    );
    public static function get($name, $index=NULL) {
        if ($index === NULL) {
            return isset(self::$config[$name]) ? self::$config[$name] : FALSE;
        } else {
            return isset(self::$config[$name][$index]) ? self::$config[$name][$index] : FALSE;
        }
    }
    public static function exists($name) {
        return isset(self::$config[$name]);
    }
    public static function storagePoolDrives() {
        return self::get(CONFIG_STORAGE_POOL_DRIVE);
    }
    public static function set($name, $value) {
        self::$config[$name] = $value;
    }
    public static function add($name, $value, $index=NULL) {
        if ($index === NULL) {
            self::$config[$name][] = $value;
        } else {
            self::$config[$name][$index] = $value;
        }
    }
}
final class SharesConfig {
    private static $shares_config;
    private static function _getConfig($share) {
        if (!self::exists($share)) {
            self::$shares_config[$share] = array();
        }
        return self::$shares_config[$share];
    }
    public static function exists($share) {
        return isset(self::$shares_config[$share]);
    }
    public static function getShares() {
        $result = array();
        $all_shares = self::$shares_config;
        if (!is_array($all_shares)) {
            $all_shares = [];
        }
        foreach ($all_shares as $share_name => $share_config) {
            if ($share_name != CONFIG_TRASH_SHARE) {
                $result[$share_name] = $share_config;
            }
        }
        return $result;
    }
    public static function getConfigForShare($share) {
        if (!self::exists($share)) {
            return FALSE;
        }
        return self::$shares_config[$share];
    }
    public static function removeShare($share) {
        unset(self::$shares_config[$share]);
    }
    public static function remove($share, $name) {
        unset(self::$shares_config[$share][$name]);
    }
    public static function get($share, $name, $index=NULL) {
        if (!self::exists($share)) {
            return FALSE;
        }
        $config = self::$shares_config[$share];
        if ($index === NULL) {
            return isset($config[$name]) ? $config[$name] : FALSE;
        } else {
            return isset($config[$name][$index]) ? $config[$name][$index] : FALSE;
        }
    }
    public static function set($share, $name, $value) {
        $config = self::_getConfig($share);
        $config[$name] = $value;
        self::$shares_config[$share] = $config;
    }
    public static function add($share, $name, $value, $index=NULL) {
        $config = self::_getConfig($share);
        if ($index === NULL) {
            $config[$name][] = $value;
        } else {
            $config[$name][$index] = $value;
        }
        self::$shares_config[$share] = $config;
    }
    public static function getNumCopies($share) {
        $num_copies = static::get($share, CONFIG_NUM_COPIES);
        if (!$num_copies) {
            Log::warn("Found a task on a share ($share) that disappeared from " . ConfigHelper::$config_file . ". Skipping.", Log::EVENT_CODE_TASK_FOR_UNKNOWN_SHARE);
            return -1;
        }
        if ($num_copies < 1) {
            $num_copies = 1;
        }
        $max_copies = 0;
        foreach (Config::storagePoolDrives() as $sp_drive) {
            if (StoragePool::is_pool_drive($sp_drive)) {
                $max_copies++;
            }
        }
        if ($num_copies > $max_copies) {
            $num_copies = $max_copies;
        }
        return $num_copies;
    }
    public static function getShareOptions($full_path) {
        $share = FALSE;
        $landing_zone = '';
        foreach (SharesConfig::getShares() as $share_name => $share_options) {
            $lz = $share_options[CONFIG_LANDING_ZONE];
            if (string_starts_with($full_path, $lz) && mb_strlen($lz) > mb_strlen($landing_zone)) {
                $landing_zone = $lz;
                $share = $share_options;
            }
        }
        return $share;
    }
    public static function getShareOptionsFromDrive($full_path, $sp_drive) {
        $landing_zone = '';
        $share = FALSE;
        foreach (SharesConfig::getShares() as $share_name => $share_options) {
            $lz = $share_options[CONFIG_LANDING_ZONE];
            $metastore = Metastores::get_metastore_from_path($full_path);
            if ($metastore !== FALSE) {
                if (string_starts_with($full_path, "$metastore/$share_name") && mb_strlen($lz) > mb_strlen($landing_zone)) {
                    $landing_zone = $lz;
                    $share = $share_options;
                }
            } else {
                if (string_starts_with($full_path, "$sp_drive/$share_name") && mb_strlen($lz) > mb_strlen($landing_zone)) {
                    $landing_zone = $lz;
                    $share = $share_options;
                }
            }
        }
        return $share;
    }
}

final class DB {
	protected static $options; // connection options
	protected static $handle; // database handle
    public static function isConnected() {
        return (bool) self::$handle;
    }
	public static function setOptions($options) {
		self::$options = to_object($options);
	}
	public static function connect($retry_until_successful=FALSE, $throw_exception_on_error=FALSE, $timeout = 10) {
        $connect_string = 'mysql:host=' . self::$options->host . ';dbname=' . self::$options->name . ';charset=utf8mb4';
        try {
            self::$handle = @new PDO($connect_string, self::$options->user, self::$options->pass, array(PDO::ATTR_TIMEOUT => $timeout, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
        } catch (PDOException $ex) {
            if ($retry_until_successful) {
                sleep(2);
                return DB::connect(TRUE);
            }
            if ($throw_exception_on_error) {
                throw new Exception("Can't connect to database: " . $ex->getMessage(), $ex->getCode(), $ex);
            } else {
                echo "ERROR: Can't connect to database: " . $ex->getMessage() . "\n";
                Log::critical("Can't connect to database: " . $ex->getMessage(), Log::EVENT_CODE_DB_CONNECT_FAILED);
            }
        }
        if (self::$handle) {
            DB::execute("SET SESSION group_concat_max_len = 1048576");
            DB::execute("SET SESSION wait_timeout = 86400"); # Allow 24h fsck!
            if (self::$options->name != 'mysql') {
                DB::migrate();
            }
            $now = DB::getFirstValue("SELECT NOW()");
            $diff = time() - strtotime($now);
            if (abs($diff) > 20*60) {
                $symbol = $diff < 0 ? '-' : '+';
                $diff_minutes = round(abs($diff)/60);
                $diff_hours = floor($diff_minutes/60);
                $diff_minutes -= $diff_hours*60;
                $mysql_tz = sprintf("%s%02d:%02d", $symbol, $diff_hours, $diff_minutes);
                Log::info("Adjusting MySQL Timezone: $diff secs difference between MySQL and PHP => Changing MySQL TZ to '$mysql_tz'");
                try {
                    DB::execute("SET time_zone = :tz", ['tz' => $mysql_tz]);
                } catch (Exception $ex) {
                    Log::error("Tried to change MySQL's timezone to $mysql_tz, since the system's TZ and MySQL's TZ don't match, and that failed. Error: " . $ex->getMessage() . " To fix this issue, change either the system's or MySQL's timezone so that both match.", Log::EVENT_CODE_DB_TZ_CHANGE_FAILED);
                }
            }
        }
        return self::$handle;
    }
    public static function execute($q, $args = array(), $attempt_repair=TRUE) {
        $stmt = self::$handle->prepare($q);
        foreach ($args as $key => $value) {
            $stmt->bindValue(":$key", $value);
        }
        try {
            $stmt->execute();
            return $stmt;
        } catch (PDOException $e) {
            $error = $e->errorInfo;
            if (($error[1] == 144 || $error[1] == 145) && $attempt_repair) {
                Log::info("Error during MySQL query: " . $e->getMessage() . '. Will now try to repair the MySQL tables.');
                DB::repairTables();
                return DB::execute($q, $args, FALSE); // $attempt_repair = FALSE, to not go into an infinite loop, if the repair doesn't work.
            }
            if ($error[1] == 1406 && $attempt_repair) {
                Log::info("Error during MySQL query: " . $e->getMessage() . '. Will now try to use larger full_path columns.');
                DB::migrate_large_fullpath();
                return DB::execute($q, $args, FALSE); // $attempt_repair = FALSE, to not go into an infinite loop, if the fix doesn't work.
            }
            throw new Exception($e->getMessage(), $error[1]);
        }
    }
    public static function insert($q, $args = array()) {
        if (DB::execute($q, $args) === FALSE) {
            return FALSE;
        }
        return DB::lastInsertedId();
    }
    public static function getFirst($q, $args = array()) {
        $stmt = DB::execute($q, $args);
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($result === FALSE) {
            return FALSE;
        }
        return (object) $result;
    }
    public static function getFirstValue($q, $args = array()) {
        $row = DB::getFirst($q, $args);
        if (empty($row)) {
            return FALSE;
        }
        $row = (array) $row;
        return array_shift($row);
    }
    public static function getAll($q, $args = array(), $index_field=NULL) {
        $stmt = DB::execute($q, $args);
        $rows = array();
        $i = 0;
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            $index = $i++;
            if (!empty($index_field)) {
                $index = $row[$index_field];
            }
            $rows[$index] = (object) $row;
        }
        return $rows;
    }
    public static function getAllValues($q, $args = array(), $data_type=null) {
        $stmt = DB::execute($q, $args);
        $values = array();
        while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
            if (!is_array($row)) {
                return FALSE;
            }
            $value = array_shift($row);
            if (!empty($data_type)) {
                settype($value, $data_type);
            }
            $values[] = $value;
        }
        return $values;
    }
    public static function lastInsertedId() {
        $q = "SELECT LAST_INSERT_ID()";
        $lastInsertedId = (int) DB::getFirstValue($q);
        if ($lastInsertedId === 0) {
            return TRUE;
        }
        return $lastInsertedId;
    }
    public static function acquireLock($name, $timeout = NULL) {
        $locked = static::getFirstValue("SELECT GET_LOCK(:name, :timeout)", ['name' => $name, 'timeout' => $timeout]);
        if ($locked) {
            return TRUE;
        }
        return FALSE;
    }
    public static function releaseLock($name) {
        $released = static::getFirstValue("SELECT RELEASE_LOCK(:name)", ['name' => $name]);
        if (!$released && static::isLocked($name)) {
            return FALSE;
        }
        return TRUE;
    }
    public static function isLocked($name) {
        $is_lock_free = static::getFirstValue("SELECT IS_FREE_LOCK(:name)", $name);
        return !$is_lock_free;
    }
    public static function error() {
        return self::$options->error;
    }
    private static function migrate() {
        $db_version = (int) Settings::get('db_version');
        if ($db_version < 11) {
            DB::migrate_1_frozen_thaw();
            DB::migrate_2_idle();
            DB::migrate_3_larger_settings();
            DB::migrate_4_find_next_task_index();
            DB::migrate_5_find_next_task_index();
            DB::migrate_6_md5_worker_indexes();
            DB::migrate_7_larger_full_path();
            DB::migrate_8_du_stats();
            DB::migrate_9_complete_writen();
            DB::migrate_10_utf8();
            DB::migrate_11_varchar();
        }
        if ($db_version < 12) {
            DB::migrate_12_force_update_complete();
            Settings::set('db_version', 12);
        }
        if ($db_version < 13) {
            DB::migrate_13_checksums();
            Settings::set('db_version', 13);
        }
        if ($db_version < 14) {
            DB::migrate_14_status();
            Settings::set('db_version', 14);
        }
        if ($db_version < 15) {
            DB::migrate_15_status_myisam();
            Settings::set('db_version', 15);
        }
        if ($db_version < 16) {
            DB::migrate_16_larger_action();
            Settings::set('db_version', 16);
        }
        if ($db_version < 17) {
            DB::migrate_17_status_action();
            Settings::set('db_version', 17);
        }
        if ($db_version < 18) {
            DB::migrate_18_full_path_utf8mb4();
            Settings::set('db_version', 18);
        }
    }
    private static function migrate_1_frozen_thaw() {
    }
    private static function migrate_2_idle() {
    }
    private static function migrate_3_larger_settings() {
        $query = "DESCRIBE settings";
        $rows = DB::getAll($query);
        foreach ($rows as $row) {
            if ($row->Field == 'value') {
                if ($row->Type == "tinytext") {
                    DB::execute("ALTER TABLE settings CHANGE value value TEXT CHARACTER SET utf8 NOT NULL");
                }
                break;
            }
        }
    }
    private static function migrate_4_find_next_task_index() {
        $query = "SHOW INDEX FROM tasks WHERE Key_name = 'find_next_task'";
        $row = DB::getFirst($query);
        if ($row === FALSE) {
            DB::execute("ALTER TABLE tasks ADD INDEX find_next_task (complete, share(64), id)");
        }
        $query = "SHOW INDEX FROM tasks WHERE Key_name = 'incomplete_open'";
        $row = DB::getFirst($query);
        if ($row) {
            DB::execute("ALTER TABLE tasks DROP INDEX incomplete_open");
        }
        $query = "SHOW INDEX FROM tasks WHERE Key_name = 'subsequent_writes'";
        $row = DB::getFirst($query);
        if ($row) {
            DB::execute("ALTER TABLE tasks DROP INDEX subsequent_writes");
        }
        $query = "SHOW INDEX FROM tasks WHERE Key_name = 'unneeded_unlinks'";
        $row = DB::getFirst($query);
        if ($row) {
            DB::execute("ALTER TABLE tasks DROP INDEX unneeded_unlinks");
        }
    }
    private static function migrate_5_find_next_task_index() {
        $query = "SHOW INDEX FROM tasks WHERE Key_name = 'find_next_task' and Column_name = 'share'";
        $row = DB::getFirst($query);
        if ($row !== FALSE) {
            DB::execute("ALTER TABLE tasks DROP INDEX find_next_task");
            DB::execute("ALTER TABLE tasks ADD INDEX find_next_task (complete, id)");
        }
    }
    private static function migrate_6_md5_worker_indexes() {
        $query = "SHOW INDEX FROM tasks WHERE Key_name = 'md5_worker'";
        $row = DB::getFirst($query);
        if ($row === FALSE) {
            DB::execute("ALTER TABLE tasks ADD INDEX md5_worker (action, complete, additional_info(100), id)");
        }
        $query = "SHOW INDEX FROM tasks WHERE Key_name = 'md5_checker'";
        $row = DB::getFirst($query);
        if ($row === FALSE) {
            DB::execute("ALTER TABLE tasks ADD INDEX md5_checker (action, share(64), full_path(265), complete)");
        }
        $query = "DESCRIBE tasks";
        $rows = DB::getAll($query);
        foreach ($rows as $row) {
            if ($row->Field == 'additional_info') {
                if ($row->Type == "tinytext") {
                    DB::execute("ALTER TABLE tasks CHANGE additional_info additional_info TEXT CHARACTER SET utf8 NULL");
                }
                break;
            }
        }
    }
    private static function migrate_7_larger_full_path() {
    }
    private static function migrate_8_du_stats() {
        $query = "CREATE TABLE IF NOT EXISTS `du_stats` (`share` TINYTEXT NOT NULL, `full_path` TEXT NOT NULL, `depth` TINYINT(3) UNSIGNED NOT NULL, `size` BIGINT(20) UNSIGNED NOT NULL, UNIQUE KEY `uniqness` (`share`(64),`full_path`(269))) ENGINE = MYISAM DEFAULT CHARSET=utf8";
        DB::execute($query);
        $query = "SHOW INDEX FROM `du_stats` WHERE Key_name = 'uniqness'";
        $row = DB::getFirst($query);
        if ($row === FALSE) {
            DB::execute("TRUNCATE `du_stats`");
            DB::execute("ALTER TABLE `du_stats` ADD UNIQUE `uniqness` (`share` (64), `full_path` (269))");
        }
    }
    private static function migrate_9_complete_writen() {
        $query = "DESCRIBE tasks";
        $rows = DB::getAll($query);
        foreach ($rows as $row) {
            if ($row->Field == 'complete') {
                if ($row->Type == "enum('yes','no','frozen','thawed','idle')") {
                    DB::execute("ALTER TABLE tasks CHANGE complete complete ENUM('yes','no','frozen','thawed','idle','written') NOT NULL");
                    DB::execute("ALTER TABLE tasks_completed CHANGE complete complete ENUM('yes','no','frozen','thawed','idle','written') NOT NULL");
                }
                break;
            }
        }
    }
    private static function migrate_10_utf8() {
        $q = "SHOW INDEX FROM du_stats WHERE key_name = 'uniqness' AND column_name = 'full_path'";
        $index_def = DB::getFirst($q);
        if ($index_def->Sub_part > 269) {
            $q = "ALTER TABLE du_stats DROP INDEX uniqness";
            DB::execute($q);
            $q = "ALTER TABLE du_stats ADD UNIQUE INDEX `uniqness` (share(64), full_path(269))";
            DB::execute($q);
        }
        $q = "SHOW INDEX FROM tasks WHERE key_name = 'md5_checker' AND column_name = 'full_path'";
        $index_def = DB::getFirst($q);
        if ($index_def->Sub_part > 265) {
            $q = "ALTER TABLE tasks DROP INDEX md5_checker";
            DB::execute($q);
            $q = "ALTER TABLE tasks ADD INDEX `md5_checker` (action, share(64), full_path(265), complete)";
            DB::execute($q);
        }
        $tables = array(
            'du_stats',
            'settings',
            'tasks',
            'tasks_completed'
        );
        $columns = array(
            'du_stats|share|TINYTEXT CHARACTER SET utf8 NOT NULL',
            'du_stats|full_path|TEXT CHARACTER SET utf8 NOT NULL',
            'settings|name|TINYTEXT CHARACTER SET utf8 NOT NULL',
            'settings|value|TEXT CHARACTER SET utf8 NOT NULL',
            'tasks|share|TINYTEXT CHARACTER SET utf8 NOT NULL',
            'tasks|full_path|TEXT CHARACTER SET utf8 NULL',
            'tasks|additional_info|TEXT CHARACTER SET utf8 NULL',
            'tasks_completed|share|TINYTEXT CHARACTER SET utf8 NOT NULL',
            'tasks_completed|full_path|TEXT CHARACTER SET utf8 NULL',
            'tasks_completed|additional_info|TEXT CHARACTER SET utf8 NULL',
        );
        $query = "SELECT CCSA.character_set_name FROM information_schema.`TABLES` T, information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` CCSA WHERE CCSA.collation_name = T.table_collation AND T.table_schema = :schema AND T.table_name = :table";
        foreach ($tables as $table_name) {
            $charset = DB::getFirstValue($query, array('schema' => Config::get(CONFIG_DB_NAME), 'table' => $table_name));
            if ($charset != "utf8") {
                Log::info("Updating $table_name table to UTF-8");
                try {
                    DB::execute("ALTER TABLE `$table_name` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci");
                } catch (Exception $ex) {
                    try {
                        DB::execute("ALTER TABLE `$table_name` CHARACTER SET utf8 COLLATE utf8_general_ci");
                    } catch (Exception $ex) {
                        Log::warn("  ALTER TABLE failed.", Log::EVENT_CODE_DB_MIGRATION_FAILED);
                    }
                }
            }
        }
        $query = "SELECT character_set_name FROM information_schema.`COLUMNS` C WHERE table_schema = :schema AND table_name = :table AND column_name = :field";
        foreach ($columns as $value) {
            list($table_name, $column_name, $definition) = explode('|', $value);
            $charset = DB::getFirstValue($query, array('schema' => Config::get(CONFIG_DB_NAME), 'table' => $table_name, 'field' => $column_name));
            if ($charset != "utf8") {
                Log::info("Updating $table_name.$column_name column to UTF-8");
                DB::execute("ALTER TABLE `$table_name` CHANGE `$column_name` `$column_name` $definition");
            }
        }
    }
    private static function migrate_11_varchar() {
        $q = "ALTER TABLE `settings` CHANGE `name` `name` VARCHAR(255) NOT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` DROP INDEX `md5_checker`";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` CHANGE `share` `share` VARCHAR(255) NOT NULL, CHANGE `full_path` `full_path` VARCHAR(255) NULL, CHANGE `additional_info` `additional_info` VARCHAR(255) NULL";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` ADD INDEX `md5_checker` (`action`, `share`(64), `full_path`, `complete`)";
        DB::execute($q);
        $q = "ALTER TABLE `tasks_completed` CHANGE `share` `share` VARCHAR(255) NOT NULL, CHANGE `full_path` `full_path` VARCHAR(255) NULL, CHANGE `additional_info` `additional_info` VARCHAR(255) NULL";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` DROP INDEX `uniqness`";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` CHANGE `share` `share` VARCHAR(255) NOT NULL, CHANGE `full_path` `full_path` VARCHAR(255) NOT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` ADD UNIQUE KEY `uniqness` (`share`(64),`full_path`)";
        DB::execute($q);
    }
    private static function migrate_12_force_update_complete() {
        DB::execute("ALTER TABLE tasks CHANGE complete complete ENUM('yes','no','frozen','thawed','idle','written') CHARACTER SET ascii NOT NULL");
        DB::execute("ALTER TABLE tasks_completed CHANGE complete complete ENUM('yes','no','frozen','thawed','idle','written') CHARACTER SET ascii NOT NULL");
    }
    private static function migrate_large_fullpath() {
        $q = "ALTER TABLE `settings` DROP PRIMARY KEY";
        DB::execute($q);
        $q = "ALTER TABLE `settings` CHANGE `name` `name` TEXT NOT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `settings` ADD PRIMARY KEY (`name`(255))";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` DROP INDEX `md5_checker`";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` CHANGE `full_path` `full_path` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `additional_info` `additional_info` TEXT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` ADD INDEX `md5_checker` (`action`, `share`(64), `full_path`(180), `complete`)";
        DB::execute($q);
        $q = "ALTER TABLE `tasks_completed` CHANGE `full_path` `full_path` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `additional_info` `additional_info` TEXT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` DROP INDEX `uniqness`";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` CHANGE `full_path` `full_path` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` ADD UNIQUE KEY `uniqness` (`share`(64),`full_path`(200))";
        DB::execute($q);
    }
    private static function migrate_13_checksums() {
        $query = "CREATE TABLE IF NOT EXISTS `checksums` (`id` char(32) NOT NULL DEFAULT '', `share` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', `full_path` text CHARACTER SET utf8 NOT NULL, `checksum` char(32) NOT NULL DEFAULT '', `last_checked` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), PRIMARY KEY (`id`)) ENGINE = MYISAM DEFAULT CHARSET=ascii";
        DB::execute($query);
    }
    private static function migrate_14_status() {
        $query = "CREATE TABLE IF NOT EXISTS `status` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,`date_time` timestamp NOT NULL DEFAULT current_timestamp(),`action` enum('initialize','unknown','daemon','pause','resume','fsck','balance','stats','status','logs','trash','queue','iostat','getuid','worker','symlinks','replace','for','gone','going','thaw','debug','metadata','share','check_pool','sleep','read_smb_spool','fsck_file') DEFAULT NULL,`log` text NOT NULL,UNIQUE KEY `id` (`id`)) ENGINE=MYISAM DEFAULT CHARSET=utf8";
        DB::execute($query);
    }
    private static function migrate_15_status_myisam() {
        $query = "ALTER TABLE `status` ENGINE = MYISAM";
        DB::execute($query);
    }
    private static function migrate_16_larger_action() {
        $query = "ALTER TABLE `tasks` CHANGE `action` `action` varchar(12) CHARACTER SET ascii NOT NULL DEFAULT ''";
        DB::execute($query);
        $query = "ALTER TABLE `tasks_completed` CHANGE `action` `action` varchar(12) CHARACTER SET ascii NOT NULL DEFAULT ''";
        DB::execute($query);
    }
    private static function migrate_17_status_action() {
        $query = "ALTER TABLE `status` CHANGE `action` `action` varchar(16) CHARACTER SET latin1 COLLATE latin1_general_ci DEFAULT NULL";
        DB::execute($query);
    }
    private static function migrate_18_full_path_utf8mb4() {
        $q = "ALTER TABLE `checksums` CHANGE `full_path` `full_path` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL";
        DB::execute($q);
        $query = "DESCRIBE tasks";
        $rows = DB::getAll($query);
        foreach ($rows as $row) {
            if ($row->Field == 'full_path') {
                if ($row->Type == 'text') {
                    DB::migrate_large_fullpath();
                    return;
                }
                break;
            }
        }
        $q = "ALTER TABLE `tasks` DROP INDEX `md5_checker`";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` CHANGE `full_path` `full_path` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `additional_info` `additional_info` TEXT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `tasks` ADD INDEX `md5_checker` (`action`, `share`(64), `full_path`(180), `complete`)";
        DB::execute($q);
        $q = "ALTER TABLE `tasks_completed` CHANGE `full_path` `full_path` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, CHANGE `additional_info` `additional_info` TEXT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` DROP INDEX `uniqness`";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` CHANGE `full_path` `full_path` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL";
        DB::execute($q);
        $q = "ALTER TABLE `du_stats` ADD UNIQUE KEY `uniqness` (`share`(64),`full_path`(200))";
        DB::execute($q);
    }
    public static function repairTables() {
        if (Log::actionIs(ACTION_DAEMON)) {
            Log::info("Checking MySQL tables...");
        }
        foreach (array('tasks', 'settings', 'du_stats', 'tasks_completed') as $table_name) {
            try {
                DB::execute("SELECT * FROM $table_name LIMIT 1", array(), FALSE);
            } catch (Exception $e) {
                Log::warn("Test failed for $table_name MySQL table: " . $e->getMessage() . " - Will try to repair it using: REPAIR TABLE $table_name ...", Log::EVENT_CODE_DB_TABLE_CRASHED);
                DB::execute("REPAIR TABLE $table_name", array(), FALSE);
            }
        }
    }
    public static function deleteExecutedTasks() {
        $executed_tasks_retention = Config::get(CONFIG_EXECUTED_TASKS_RETENTION);
        if ($executed_tasks_retention == 'forever') {
            return;
        }
        if (!is_int($executed_tasks_retention)) {
            Log::critical("Error: Invalid value for 'executed_tasks_retention' in greyhole.conf: '$executed_tasks_retention'. You need to use either 'forever' (no quotes), or a number of days.", Log::EVENT_CODE_CONFIG_INVALID_VALUE);
        }
        Log::info("Cleaning executed tasks: keeping the last $executed_tasks_retention days of logs.");
        $query = sprintf("DELETE FROM tasks_completed WHERE event_date < NOW() - INTERVAL %d DAY", (int) $executed_tasks_retention);
        DB::execute($query);
    }
}

class BalanceTask extends AbstractTask {
    private $skip_stickies = FALSE;
    public function execute() {
        Log::info("Starting available space balancing");
        $compare_share_balance = function ($a, $b) {
            if (static::is_share_sticky($a['name']) && !static::is_share_sticky($b['name'])) {
                return -1;
            }
            if (!static::is_share_sticky($a['name']) && static::is_share_sticky($b['name'])) {
                return 1;
            }
            if ($a[CONFIG_NUM_COPIES] != $b[CONFIG_NUM_COPIES]) {
                return $a[CONFIG_NUM_COPIES] > $b[CONFIG_NUM_COPIES] ? -1 : 1;
            }
            return rand(0, 1) ? -1 : 1;
        };
        $sorted_shares_options = SharesConfig::getShares();
        unset($sorted_shares_options[CONFIG_TRASH_SHARE]);
        uasort($sorted_shares_options, $compare_share_balance);
        Log::debug("┌ Will balance the shares in the following order: " . implode(", ", array_keys($sorted_shares_options)));
        foreach ([100, 10, 5, 1] as $min_file_size) {
            foreach ($sorted_shares_options as $share => $share_options) {
                if ($share_options[CONFIG_NUM_COPIES] == count(Config::storagePoolDrives())) {
                    Log::debug("├ Skipping share $share; has num_copies = max");
                    continue;
                }
                if ($this->skip_stickies && static::is_share_sticky($share)) {
                    Log::debug("├ Skipping share $share; is sticky");
                    continue;
                }
                $this->balance_share($share, $share_options, $min_file_size);
            }
        }
        Log::debug("└ Done balancing all shares");
        if ($this->skip_stickies) {
            $arr = debug_backtrace();
            if (count($arr) < 93) {
                Log::debug("Some shares with sticky files were skipped. Balancing will now re-start to continue moving those sticky files as needed, and further balance. Recursion level = " . count($arr));
                return $this->execute();
            }
            Log::info("Maximum number of consecutive balance reached. You'll need to re-execute --balance if you want to balance further.");
        }
        Log::info("Available space balancing completed.");
        return TRUE;
    }
    private function balance_share($share, $share_options, $min_file_size) {
        Log::debug("├┐ Balancing share: $share ({$min_file_size}MB or + files)");
        $drives_selectors = Config::get(CONFIG_DRIVE_SELECTION_ALGORITHM);
        $all_drives = array();
        foreach ($drives_selectors as $ds) {
            $all_drives[] = $ds->drives;
        }
        $drives_selectors_share = SharesConfig::get($share, CONFIG_DRIVE_SELECTION_ALGORITHM);
        $share_drives = array();
        foreach ($drives_selectors_share as $ds) {
            $share_drives[] = $ds->drives;
        }
        if ($share_drives !== $all_drives) {
            Log::debug("├┘ Won't balance using share $share, because it uses a custom 'drive_selection_algorithm' config.");
            return;
        }
        foreach ($drives_selectors as $ds) {
            $pool_drives_avail_space = StoragePool::get_drives_available_space();
            $balance_direction_asc = array();
            foreach ($pool_drives_avail_space as $drive => $available_space) {
                if (!array_contains($ds->drives, $drive)) {
                    unset($pool_drives_avail_space[$drive]);
                    continue;
                }
                $target_avail_space = array_sum($pool_drives_avail_space) / count($pool_drives_avail_space);
                $balance_direction_asc[$drive] = $pool_drives_avail_space[$drive] < $target_avail_space;
            }
            if (count($pool_drives_avail_space) > 0) {
                $target_avail_space = array_sum($pool_drives_avail_space) / count($pool_drives_avail_space);
                $sort_fct = function ($drive1, $drive2) use ($pool_drives_avail_space, $target_avail_space) {
                    $delta_needed1 = $target_avail_space - $pool_drives_avail_space[$drive1];
                    $delta_needed2 = $target_avail_space - $pool_drives_avail_space[$drive2];
                    return $delta_needed1 > $delta_needed2 ? -1 : 1;
                };
                uksort($pool_drives_avail_space, $sort_fct);
                Log::debug("│├ Will work on the storage pool drives in the following order: " . implode(", ", array_keys($pool_drives_avail_space)));
            }
            foreach ($pool_drives_avail_space as $source_drive => $current_avail_space) {
                $this->balance_drive($share, $share_options, $source_drive, $pool_drives_avail_space, $balance_direction_asc, $min_file_size);
            }
        }
        Log::debug("├┘ Done balancing share: $share ({$min_file_size}MB or + files)");
    }
    private function balance_drive($share, $share_options, $source_drive, &$pool_drives_avail_space, $balance_direction_asc, $min_file_size) {
        $current_avail_space = $pool_drives_avail_space[$source_drive];
        $target_avail_space = array_sum($pool_drives_avail_space) / count($pool_drives_avail_space);
        $delta_needed = $target_avail_space - $current_avail_space;
        if ($delta_needed <= 10*1024 || $delta_needed < $min_file_size*1024) {
            Log::debug("│├ Skipping balancing storage pool drive: $source_drive; it has enough available space: (". bytes_to_human($current_avail_space*1024, FALSE) ." available, target: ". bytes_to_human($target_avail_space*1024, FALSE) .")");
            return;
        }
        Log::debug("│├┐ Balancing storage pool drive: $source_drive (". bytes_to_human($current_avail_space*1024, FALSE) ." available, target: ". bytes_to_human($target_avail_space*1024, FALSE) .")");
        $files = array();
        if (is_dir("$source_drive/$share")) {
            $max_file_size = floor($delta_needed / 1024);
            $command = "find " . escapeshellarg("$source_drive/$share") . " -type f -size +{$min_file_size}M -size -{$max_file_size}M";
            exec($command, $files);
        }
        if (count($files) == 0) {
            Log::debug("│├┘ Found no files that could be moved.");
            return;
        }
        Log::debug("│││ Found ". count($files) ." files that can be moved.");
        $file_too_large_warnings = 0;
        foreach ($files as $file) {
            if (!$this->balance_file($file, $share, $share_options, $source_drive, $pool_drives_avail_space, $balance_direction_asc, $file_too_large_warnings)) {
                break;
            }
        }
        $delta_needed = $target_avail_space - $current_avail_space;
        if ($delta_needed > 50*1024) {
            Log::debug("│├┘ Balancing storage pool drive $source_drive finished before enough space was made available: (" . bytes_to_human($current_avail_space * 1024, FALSE) . " available, target: ". bytes_to_human($target_avail_space*1024, FALSE) .")");
        }
    }
    private function balance_file($file, $share, $share_options, $source_drive, &$pool_drives_avail_space, $balance_direction_asc, &$file_too_large_warnings) {
        $num_total_drives = count($pool_drives_avail_space);
        $current_avail_space = $pool_drives_avail_space[$source_drive];
        $target_avail_space = array_sum($pool_drives_avail_space) / count($pool_drives_avail_space);
        $delta_needed = $target_avail_space - $current_avail_space;
        if ($delta_needed <= 50*1024) {
            Log::debug("│├┘ Storage pool drive $source_drive now has enough available space: (". bytes_to_human($current_avail_space*1024, FALSE) ." available, target: ". bytes_to_human($target_avail_space*1024, FALSE) .")");
            return FALSE;
        }
        if (gh_is_file_locked($file) !== FALSE) {
            Log::debug("││├ File $file is locked by another process. Skipping.");
            return TRUE;
        }
        $filesize = gh_filesize($file)/1024; // KB
        if ($filesize > $delta_needed) {
            if (++$file_too_large_warnings >= 20) {
                Log::debug("││├ $file_too_large_warnings files too large in a row. Stopping balance of $source_drive/$share");
                return FALSE;
            }
            Log::debug("││├ File is too large (" .  bytes_to_human($filesize*1024, FALSE) . "). Skipping.");
            return TRUE;
        }
        $file_too_large_warnings = 0;
        $full_path = mb_substr($file, mb_strlen("$source_drive/$share/"));
        list($path, ) = explode_full_path($full_path);
        Log::debug("││├┐ Working on file: $share/$full_path (". bytes_to_human($filesize*1024, FALSE) .")");
        $target_drives = StoragePool::choose_target_drives($filesize, FALSE, $share, $path, '  ', $is_sticky);
        unset($sp_drive);
        if ($is_sticky) {
            if (count($target_drives) == $num_total_drives - 1 && !array_contains($target_drives, $source_drive)) {
            } else if (count($target_drives) < $num_total_drives) {
                $this->skip_stickies = TRUE;
                Log::debug("│├┴┘ Some drives are full. Skipping sticky shares until all drives have some free space.");
                return FALSE;
            }
            $sticky_drives = array_slice($target_drives, 0, SharesConfig::getNumCopies($share));
            if (array_contains($sticky_drives, $source_drive)) {
                Log::debug("││├┘ Source is sticky. Skipping.");
                return TRUE;
            }
            $already_stuck_copies = 0;
            foreach ($sticky_drives as $drive) {
                if (file_exists("$drive/$share/$full_path")) {
                    $already_stuck_copies++;
                } else {
                    $sp_drive = $drive;
                }
            }
        } else {
            while (count($target_drives) > 0) {
                $drive = array_shift($target_drives);
                if (!file_exists("$drive/$share/$full_path") && array_contains(array_keys($pool_drives_avail_space), $drive)) {
                    $sp_drive = $drive;
                    break;
                }
            }
        }
        if (!isset($sp_drive)) {
            if ($is_sticky) {
                Log::debug("││├┘ Sticky file is already where it should be. Skipping.");
            }
            return TRUE;
        }
        Log::debug("││││ Target drive: $sp_drive (". bytes_to_human($pool_drives_avail_space[$sp_drive]*1024, FALSE) ." available)");
        if ($is_sticky) {
            Log::debug("││││ Moving sticky file, even if that means it won't help balancing available space.");
        } else {
            $new_drive_needs_more_avail_space = $balance_direction_asc[$sp_drive];
            $new_drive_needs_less_avail_space = !$new_drive_needs_more_avail_space;
            $new_drive_avail_space = $pool_drives_avail_space[$sp_drive];
            if ($new_drive_needs_more_avail_space && $new_drive_avail_space <= $target_avail_space) {
                Log::debug("││├┘ Target drive needs more available space; moving a file there would do the opposite. Skipping.");
                return TRUE;
            }
            if ($new_drive_needs_less_avail_space && $new_drive_avail_space <= $target_avail_space) {
                Log::debug("││├┘ Target drive needed less available space; is low enough now. Skipping.");
                return TRUE;
            }
        }
        $original_path = clean_dir("$source_drive/$share/$path");
        list($target_path, $filename) = explode_full_path("$sp_drive/$share/$full_path");
        gh_mkdir($target_path, $original_path);
        $temp_path = StorageFile::get_temp_filename("$sp_drive/$share/$full_path");
        $file_permissions = StorageFile::get_file_permissions($file);
        Log::debug("││││ Moving file copy...");
        $it_worked = gh_rename($file, $temp_path);
        if ($it_worked) {
            gh_rename($temp_path, "$sp_drive/$share/$full_path");
            StorageFile::set_file_permissions("$sp_drive/$share/$full_path", $file_permissions);
            $pool_drives_avail_space[$sp_drive] -= $filesize;
            $pool_drives_avail_space[$source_drive] += $filesize;
        } else {
            Log::warn("││├┘ Failed file copy. Skipping.", Log::EVENT_CODE_FILE_COPY_FAILED);
            @unlink($temp_path);
            return TRUE;
        }
        foreach (Metastores::get_metafiles($share, $path, $filename, TRUE, TRUE, FALSE) as $existing_metafiles) {
            foreach ($existing_metafiles as $key => $metafile) {
                if ($metafile->path == $file) {
                    $metafile->path = "$sp_drive/$share/$full_path";
                    unset($existing_metafiles[$key]);
                    $metafile->state = Metafile::STATE_OK;
                    if ($metafile->is_linked) {
                        $landing_zone = $share_options[CONFIG_LANDING_ZONE];
                        Log::debug("││││ Updating symlink at $landing_zone/$full_path to point to $metafile->path");
                        if (is_link("$landing_zone/$full_path")) {
                            Trash::trash_file("$landing_zone/$full_path");
                        }
                        @gh_symlink($metafile->path, "$landing_zone/$full_path");
                    }
                    $existing_metafiles[$metafile->path] = $metafile;
                    Metastores::save_metafiles($share, $path, $filename, $existing_metafiles);
                    break;
                }
            }
        }
        $current_avail_space = $pool_drives_avail_space[$source_drive];
        $target_avail_space = array_sum($pool_drives_avail_space) / count($pool_drives_avail_space);
        Log::debug("││├┘ Balancing storage pool drive: $source_drive (". bytes_to_human($current_avail_space*1024, FALSE) ." available, target: ". bytes_to_human($target_avail_space*1024, FALSE) .")");
        return TRUE;
    }
    private static function is_share_sticky($share_name) {
        $sticky_files = Config::get(CONFIG_STICKY_FILES);
        if (!empty($sticky_files)) {
            foreach ($sticky_files as $share_dir => $stick_into) {
                if (string_starts_with($share_dir, $share_name)) {
                    return TRUE;
                }
            }
        }
        return FALSE;
    }
}

class FsckTask extends AbstractTask {
    protected static $current_task;
    private $fsck_report;
    public function __construct($task) {
        parent::__construct($task);
        static::$current_task = $this;
    }
    public static function getCurrentTask($task = array()) {
        if (empty(static::$current_task)) {
            static::$current_task = new self($task);
        }
        return static::$current_task;
    }
    public function execute() {
        $new_conf_hash = static::get_conf_hash();
        if ($this->has_option(OPTION_IF_CONF_CHANGED)) {
            $last_hash = Settings::get('last_fsck_conf_md5');
            if ($new_conf_hash == $last_hash) {
                Log::info("Skipping fsck; --if-conf-changed was specified, and the configuration file didn't change since the last fsck.");
                return TRUE;
            }
        }
        $fscked_dir = $this->share;
        $where_clause = "";
        $params = array();
        $max_lz_length = 0;
        foreach (SharesConfig::getShares() as $share_name => $share_options) {
            if (strpos($fscked_dir, $share_options[CONFIG_LANDING_ZONE]) === 0 && strlen($share_options[CONFIG_LANDING_ZONE]) > $max_lz_length) {
                $max_lz_length = strlen($share_options[CONFIG_LANDING_ZONE]);
                $where_clause = "AND share = :share";
                $params['share'] = $share_name;
            }
        }
        DB::execute("DELETE FROM tasks WHERE action = 'md5' $where_clause", $params);
        $query = "UPDATE tasks SET complete = 'yes' WHERE action = 'fsck_file' AND complete = 'idle' AND id < $this->id $where_clause";
        DB::execute($query, $params);
        $num_updated_rows = DB::getFirstValue("SELECT COUNT(*) AS num_updated_rows FROM tasks WHERE action = 'fsck_file' AND complete = 'yes' AND id < $this->id $where_clause", $params);
        if ($num_updated_rows > 0) {
            Log::info("Will execute all ($num_updated_rows) pending fsck_file operations for $fscked_dir before running this fsck (task ID $this->id).");
            return FALSE;
        }
        Log::info("Starting fsck for $fscked_dir");
        FSCKWorkLog::startTask($this->id);
        $this->initialize_fsck_report($fscked_dir);
        clearstatcache();
        if ($this->has_option(OPTION_CHECKSUMS)) {
            $checksums_thread_ids = Md5Task::spawn_threads_for_pool_drives();
            Log::debug("Spawned " . count($checksums_thread_ids) . " worker threads to calculate MD5 checksums. Will now wait for results, and check them as they come in.");
        }
        $storage_volume = FALSE;
        $share_options = SharesConfig::getShareOptions($fscked_dir);
        if ($share_options === FALSE) {
            $storage_volume = StoragePool::getDriveFromPath($fscked_dir);
            $share_options = SharesConfig::getShareOptionsFromDrive($fscked_dir, $storage_volume);
        }
        Log::debug("  Storage volume? " . ($storage_volume ? $storage_volume : 'No'));
        Log::debug("  Share? " . ($share_options ? $share_options['name'] : 'No'));
        if ($share_options === FALSE) {
            if ($storage_volume !== FALSE) {
                foreach (SharesConfig::getShares() as $share_name => $share_options) {
                    $this->gh_fsck("$storage_volume/$share_name", $share_name, $storage_volume);
                }
            } else {
                Log::error("Unknown folder to fsck. You should specify a storage pool folder, a metadata store folder, a shared folder, or a subdirectory of any of those.", Log::EVENT_CODE_FSCK_UNKNOWN_FOLDER);
            }
        } else {
            $share = $share_options['name'];
            $metastore = Metastores::get_metastore_from_path($fscked_dir);
            if ($storage_volume === FALSE && $metastore === FALSE) {
                $fsck_type = FSCK_TYPE_SHARE;
            } else if ($storage_volume !== FALSE) {
                $fsck_type = FSCK_TYPE_STORAGE_POOL_DRIVE;
            } else {
                $fsck_type = FSCK_TYPE_METASTORE;
            }
            if ($fsck_type == FSCK_TYPE_SHARE) {
                $subdir = trim(str_replace($share_options[CONFIG_LANDING_ZONE], '', $fscked_dir), '/');
                $this->gh_fsck_reset_du($share, $subdir);
            }
            if ($fsck_type != FSCK_TYPE_METASTORE) {
                $this->gh_fsck($fscked_dir, $share, $storage_volume);
            }
            Log::debug("  Scan metadata stores? " . ($this->has_option(OPTION_SKIP_METASTORE) ? 'No' : 'Yes'));
            if ($this->has_option(OPTION_SKIP_METASTORE) === FALSE) {
                if ($fsck_type == FSCK_TYPE_METASTORE) {
                    $subdir = str_replace("$metastore", '', $fscked_dir);
                    Log::debug("Starting metastore fsck for $metastore/$subdir");
                    $this->gh_fsck_metastore($metastore, $subdir, $share);
                } else {
                    if ($fsck_type == FSCK_TYPE_STORAGE_POOL_DRIVE) {
                        $subdir = str_replace($storage_volume, '', $fscked_dir);
                    } else {
                        $subdir = "/$share" . str_replace($share_options[CONFIG_LANDING_ZONE], '', $fscked_dir);
                    }
                    Log::debug("Starting metastores fsck for $subdir");
                    foreach (Metastores::get_metastores() as $metastore) {
                        $this->gh_fsck_metastore($metastore, $subdir, $share);
                    }
                }
            }
            if ($fsck_type != FSCK_TYPE_STORAGE_POOL_DRIVE && $this->has_option(OPTION_ORPHANED)) {
                $subdir = "/$share" . str_replace($share_options[CONFIG_LANDING_ZONE], '', $fscked_dir);
                Log::debug("Starting orphans search for $subdir");
                $this->additional_info = str_replace('checksums', '', $this->additional_info);
                foreach (Config::storagePoolDrives() as $sp_drive) {
                    if (StoragePool::is_pool_drive($sp_drive)) {
                        $this->gh_fsck("$sp_drive/$subdir", $share, $sp_drive);
                    }
                }
            }
        }
        Log::info("fsck for $fscked_dir completed.");
        Settings::set('last_fsck_conf_md5', $new_conf_hash);
        FSCKWorkLog::taskCompleted($this->id, $this->has_option(OPTION_EMAIL));
        return TRUE;
    }
    private static function get_conf_hash() {
        exec("grep -ie 'num_copies\|storage_pool_directory\|storage_pool_drive\|sticky_files' " . escapeshellarg(ConfigHelper::$config_file) . " | grep -v '^#'", $content);
        exec("grep -ie 'path\|vfs objects' " . escapeshellarg(ConfigHelper::$smb_config_file) . " | grep -v '^#'", $content);
        return md5(implode("\n", $content));
    }
    public function gh_fsck_reset_du($share, $full_path=null) {
        if (!$this->has_option(OPTION_DU)) {
            $this->additional_info .= '|' . OPTION_DU;
        }
        $params = array('share' => $share);
        if (empty($full_path)) {
            $query = "DELETE FROM du_stats WHERE share = :share";
        } else {
            $params['full_path'] = "$full_path";
            $query = "SELECT depth, size FROM du_stats WHERE share = :share AND full_path = :full_path";
            $infos = DB::getFirst($query, $params);
            if ($infos) {
                $parts = explode('/', $full_path);
                array_pop($parts);
                for ($i=$infos->depth-1; $i>0; $i--) {
                    $path = implode('/', $parts);
                    $q = "UPDATE du_stats SET size = size - :size WHERE share = :share AND full_path = :full_path";
                    DB::execute($q, ['size' => $infos->size, 'share' => $share, 'full_path' => $path]);
                    array_pop($parts);
                }
            }
            $query = "DELETE FROM du_stats WHERE share = :share AND full_path LIKE :full_path";
            $params['full_path'] = "$full_path%";
        }
        DB::execute($query, $params);
    }
    public function gh_fsck($path, $share, $storage_path = FALSE) {
        $path = clean_dir($path);
        Log::debug("Entering $path");
        $this->fsck_report->count(FSCK_COUNT_LZ_DIRS);
        foreach (Config::storagePoolDrives() as $sp_drive) {
            if (StoragePool::getDriveFromPath($path) == $sp_drive && StoragePool::is_pool_drive($sp_drive)) {
                $dir_path = str_replace(clean_dir("$sp_drive/$share"), '', $path);
                $dir_in_lz = clean_dir(get_share_landing_zone($share) . "/$dir_path");
                if (!file_exists($dir_in_lz)) {
                    Log::info("Re-creating $dir_in_lz from $path");
                    gh_mkdir($dir_in_lz, $path);
                }
                break;
            }
        }
        $handle = @opendir($path);
        if ($handle === FALSE) {
            Log::error("  Couldn't open $path to list content. Skipping...", Log::EVENT_CODE_LIST_DIR_FAILED);
            return;
        }
        while (($filename = readdir($handle)) !== FALSE) {
            if ($filename != '.' && $filename != '..') {
                $full_path = "$path/$filename";
                $file_type = @filetype($full_path);
                if ($file_type === FALSE) {
                    $file_type = @filetype(normalize_utf8_characters($full_path));
                    if ($file_type !== FALSE) {
                        $full_path = normalize_utf8_characters($full_path);
                        $path = normalize_utf8_characters($path);
                        $filename = normalize_utf8_characters($filename);
                    }
                }
                if ($file_type == 'dir') {
                    $this->gh_fsck($full_path, $share, $storage_path);
                } else {
                    $this->gh_fsck_file($path, $filename, $file_type, 'landing_zone', $share, $storage_path);
                    if ($this->has_option(OPTION_CHECKSUMS)) {
                        $count_md5 = DBSpool::get_num_tasks('md5');
                        if ($count_md5 > 1000) {
                            Md5Task::check_md5_workers();
                            while ($count_md5 > 500) {
                                Log::debug("MD5 tasks pending: $count_md5");
                                $query = "SELECT id, action, share, full_path, additional_info, complete FROM tasks WHERE complete = 'yes' AND action = 'md5' ORDER BY id LIMIT 1";
                                $task = DB::getFirst($query);
                                if ($task) {
                                    Md5Task::gh_check_md5(AbstractTask::instantiate($task));
                                } else {
                                    sleep(5);
                                }
                                $count_md5 = DBSpool::get_num_tasks('md5');
                            }
                        }
                    }
                }
            }
        }
        closedir($handle);
    }
    public function gh_fsck_metastore($root, $path, $share) {
        if (!is_dir("$root$path")) {
            $root = normalize_utf8_characters($root);
            $path = normalize_utf8_characters($path);
            if (!is_dir("$root$path")) {
                return;
            }
        }
        Log::debug("Entering metastore " . clean_dir($root . $path));
        $handle = opendir("$root$path");
        while (($filename = readdir($handle)) !== FALSE) {
            if ($filename != '.' && $filename != '..') {
                if (@is_dir("$root$path/$filename")) {
                    $this->fsck_report->count(FSCK_COUNT_META_DIRS);
                    $this->gh_fsck_metastore($root, "$path/$filename", $share);
                } else {
                    $path_parts = explode('/', $path);
                    array_shift($path_parts);
                    $share = array_shift($path_parts);
                    $landing_zone = get_share_landing_zone($share);
                    $local_path = $landing_zone . '/' . implode('/', $path_parts);
                    if (!file_exists("$local_path/$filename")) {
                        $this->gh_fsck_file($local_path, $filename, FALSE, 'metastore', $share);
                    }
                }
            }
        }
        closedir($handle);
    }
    public function gh_fsck_file($path, $filename, $file_type, $source, $share, $storage_path = FALSE, $num_retries = 1) {
        $landing_zone = get_share_landing_zone($share);
        if($storage_path === FALSE) {
            $file_path = trim(mb_substr($path, mb_strlen($landing_zone)+1), '/');
        }else{
            $file_path = trim(mb_substr($path, mb_strlen("$storage_path/$share")+1), '/');
        }
        if ($file_type === FALSE) {
            clearstatcache();
            $file_type = @filetype(normalize_utf8_characters("$path/$filename"));
            if ($file_type !== FALSE) {
                $file_path = normalize_utf8_characters($file_path);
                $path = normalize_utf8_characters($path);
                $filename = normalize_utf8_characters($filename);
            }
        }
        if (empty($this->fsck_report)) {
            $this->fsck_report = new FSCKReport('tmp');
        }
        if ($source == 'metastore') {
            $this->fsck_report->count(FSCK_COUNT_META_FILES);
        }
        if ($file_type !== FALSE) {
            $this->fsck_report->count(FSCK_COUNT_LZ_FILES);
        }
        if ($file_type == 'file') {
            if($storage_path === FALSE) {
                Log::info("$path/$filename is a file (not a symlink). Adding a new 'write' pending task for that file.");
                SambaSpool::parse_samba_spool();
                WriteTask::queue($share, clean_dir("$file_path/$filename"));
                return;
            }
        } else {
            if ($source == 'metastore') {
                SambaSpool::parse_samba_spool();
                $query = "SELECT * FROM tasks WHERE action = 'rename' AND share = :share AND full_path = :full_path";
                $task = DB::getFirst($query, array('share' => $share, 'full_path' => "$file_path/$filename"));
                if (!$task) {
                    $task = DB::getFirst($query, array('share' => $share, 'full_path' => trim("$file_path/$filename", '/')));
                }
                if (!$task && !empty($file_path)) {
                    $scanned_dir = dirname(clean_dir("$file_path/$filename"));
                    $query = "SELECT * FROM tasks WHERE action = 'rename' AND share = :share";
                    $tasks = DB::getAll($query, array('share' => $share));
                    foreach ($tasks as $t) {
                        if (string_starts_with($scanned_dir, $t->full_path)) {
                            $task = $t;
                            break;
                        }
                    }
                }
                if ($task) {
                    Log::debug("  Missing symlink in LZ for " . clean_dir("$share/$file_path/$filename") . ", but is OK because this file was renamed after fsck started.");
                    return;
                }
                $query = "SELECT * FROM tasks WHERE action = 'unlink' AND share = :share AND full_path = :full_path";
                $task = DB::getFirst($query, array('share' => $share, 'full_path' => "$file_path/$filename"));
                if (!$task) {
                    $task = DB::getFirst($query, array('share' => $share, 'full_path' => trim("$file_path/$filename", '/')));
                }
                if ($task) {
                    Log::debug("  Missing symlink in LZ for " . clean_dir("$share/$file_path/$filename") . ", but is OK because this file was deleted after fsck started.");
                    return;
                }
                if ($file_type == 'link' && !file_exists(readlink("$path/$filename"))) {
                    unlink("$path/$filename");
                    $file_type = FALSE;
                }
                if ($file_type === FALSE) {
                    if (!Log::actionIs(ACTION_FSCK_FILE)) {
                        Log::debug("  Queuing a new fsck_file task for " . clean_dir("$share/$file_path/$filename"));
                        FsckFileTask::queue($share, empty($file_path) ? $filename : clean_dir("$file_path/$filename"), $this->additional_info);
                        return;
                    }
                }
            }
        }
        if (Metastores::get_metafile_data_filename($share, $file_path, $filename) === FALSE && Metastores::get_metafile_data_filename($share, normalize_utf8_characters($file_path), normalize_utf8_characters($filename)) === FALSE) {
            $full_path = clean_dir("$path/$filename");
            if (StorageFile::is_temp_file($full_path)) {
                $this->fsck_report->found_problem(FSCK_PROBLEM_TEMP_FILE, $full_path);
                Trash::trash_file($full_path);
                return;
            }
            if ($storage_path !== FALSE) {
                if ($this->has_option(OPTION_ORPHANED)) {
                    Log::info("$full_path is an orphaned file; we'll proceed to find all copies and symlink this file appropriately.");
                    $this->fsck_report->found_problem(FSCK_COUNT_ORPHANS);
                } else {
                    Log::info("$full_path is an orphaned file, but we're not looking for orphans. For Greyhole to recognize this file, initiate a fsck with the --find-orphaned-files option.");
                    return;
                }
            }
        }
        $file_metafiles = array();
        $file_copies_inodes = StoragePool::get_file_copies_inodes($share, $file_path, $filename, $file_metafiles);
        if (count($file_metafiles) == 0) {
            $file_copies_inodes = StoragePool::get_file_copies_inodes($share, normalize_utf8_characters($file_path), normalize_utf8_characters($filename), $file_metafiles);
            if (count($file_metafiles) > 0) {
                $file_path = normalize_utf8_characters($file_path);
                $filename = normalize_utf8_characters($filename);
            }
        }
        $num_ok = count($file_copies_inodes);
        if ($num_ok == 0 && count($file_metafiles) > 0) {
            $metadata = reset($file_metafiles);
            $original_file_path = $metadata->path;
        }
        foreach (Metastores::get_metafiles($share, $file_path, $filename, TRUE) as $metafile_block) {
            foreach ($metafile_block as $metafile) {
                $inode_number = @gh_fileinode($metafile->path);
                $root_path = str_replace(clean_dir("/$share/$file_path/$filename"), '', $metafile->path);
                if ($root_path == $metafile->path) {
                    $root_path = str_replace(normalize_utf8_characters(clean_dir("/$share/$file_path/$filename")), '', normalize_utf8_characters($metafile->path));
                    if ($root_path == $metafile->path) {
                        Log::warn("Couldn't find root path for $metafile->path", Log::EVENT_CODE_FSCK_METAFILE_ROOT_PATH_NOT_FOUND);
                    }
                    if ($inode_number !== FALSE && $metafile->state == Metafile::STATE_OK) {
                        Log::debug("Found $metafile->path");
                    }
                }
                foreach ($file_metafiles as $k => $v) {
                    if ($k == $metafile->path || normalize_utf8_characters($k) == normalize_utf8_characters($metafile->path)) {
                        $metafile->path = $v->path;
                        $metafile->state = $v->state;
                        unset($file_metafiles[$k]);
                        break;
                    }
                }
                if (is_link($metafile->path)) {
                    $link_target = readlink($metafile->path);
                    if (array_contains($file_copies_inodes, $link_target)) {
                        Log::warn("Warning! Found a symlink in your storage pool: $metafile->path -> $link_target. Deleting.", Log::EVENT_CODE_FSCK_SYMLINK_FOUND_IN_STORAGE_POOL);
                        Trash::trash_file($metafile->path);
                    }
                    $inode_number = FALSE;
                }
                if ($inode_number === FALSE || !StoragePool::is_pool_drive($root_path)) {
                    $metafile->state = Metafile::STATE_GONE;
                    $metafile->is_linked = FALSE;
                    if (StoragePool::gone_ok($root_path)) {
                        $file_copies_inodes[$metafile->path] = $metafile->path;
                        $num_ok++;
                        $this->fsck_report->count(FSCK_COUNT_GONE_OK);
                    }
                } else if (is_dir($metafile->path)) {
                    Log::debug("Found a directory that should be a file! Will try to remove it, if it's empty.");
                    @rmdir($metafile->path);
                    $metafile->state = Metafile::STATE_GONE;
                    $metafile->is_linked = FALSE;
                    continue;
                } else {
                    $metafile->state = Metafile::STATE_OK;
                    if (!isset($file_metafiles[$metafile->path])) {
                        $file_copies_inodes[$inode_number] = $metafile->path;
                        $num_ok++;
                    }
                }
                $file_metafiles[clean_dir($metafile->path)] = $metafile;
            }
        }
        $num_copies_required = SharesConfig::getNumCopies($share);
        if ($num_copies_required == -1) {
            Log::warn("Tried to fsck a share that is missing from greyhole.conf. Skipping.", Log::EVENT_CODE_FSCK_UNKNOWN_SHARE);
            return;
        }
        if (count($file_copies_inodes) > 0) {
            $found_linked_metafile = FALSE;
            foreach ($file_metafiles as $metafile) {
                if ($metafile->is_linked) {
                    if (file_exists($metafile->path)) {
                        $symlink_file_path = clean_dir(get_share_landing_zone($share) . "/$file_path/$filename");
                        $found_linked_metafile = @filetype($symlink_file_path) == 'link' && readlink($symlink_file_path) == clean_dir($metafile->path);
                        $expected_file_size = gh_filesize($metafile->path);
                        $original_file_path = $metafile->path;
                        break;
                    } else {
                        $metafile->is_linked = FALSE;
                        $metafile->state = Metafile::STATE_GONE;
                    }
                }
            }
            if (!$found_linked_metafile) {
                foreach ($file_metafiles as $first_metafile) {
                    $root_path = str_replace(clean_dir("/$share/$file_path/$filename"), '', $first_metafile->path);
                    if ($first_metafile->state == Metafile::STATE_OK && StoragePool::is_pool_drive($root_path)) {
                        $first_metafile->is_linked = TRUE;
                        $expected_file_size = gh_filesize($first_metafile->path);
                        $original_file_path = $first_metafile->path;
                        break;
                    }
                }
            }
            if ($this->has_option(OPTION_DU)) {
                $du_path = trim(clean_dir("$file_path"), '/');
                do {
                    $size = ($expected_file_size * $num_copies_required);
                    if (empty($du_path)) {
                        $depth = 1;
                    } else {
                        $chars_count = count_chars($du_path, 1);
                        if (!isset($chars_count[ord('/')])) {
                            $chars_count[ord('/')] = 0;
                        }
                        $depth = $chars_count[ord('/')] + 2;
                    }
                    $query = "INSERT INTO du_stats SET share = :share, full_path = :full_path, depth = :depth, size = :size ON DUPLICATE KEY UPDATE size = size + VALUES(size)";
                    $params = array(
                        'share' => $share,
                        'full_path' => $du_path,
                        'depth' => $depth,
                        'size' => $size,
                    );
                    DB::insert($query, $params);
                    $p = mb_strrpos($du_path, '/');
                    if ($p) {
                        $du_path = mb_substr($du_path, 0, $p);
                    } else if (!empty($du_path)) {
                        $last = TRUE;
                        $du_path = '';
                    } else {
                        $last = FALSE;
                    }
                } while (!empty($du_path) || $last);
            }
            foreach ($file_copies_inodes as $key => $real_full_path) {
                if (array_contains(array_keys($file_copies_inodes), $real_full_path)) {
                    continue;
                }
                $file_size = gh_filesize($real_full_path);
                if ($file_size != $expected_file_size) {
                    if (gh_is_file_locked($real_full_path) !== FALSE || gh_is_file_locked($original_file_path) !== FALSE) {
                        continue;
                    }
                    SambaSpool::parse_samba_spool();
                    $query = "SELECT * FROM tasks WHERE action = 'write' AND share = :share AND full_path = :full_path";
                    $task = DB::getFirst($query, array('share' => $share, 'full_path' => "$file_path/$filename"));
                    if ($task) {
                        continue;
                    }
                    if ($file_size === FALSE) {
                        Log::warn("  An empty file copy was found: $real_full_path is 0 bytes. Original: $original_file_path is " . number_format($expected_file_size) . " bytes. This empty copy will be deleted.", Log::EVENT_CODE_FSCK_EMPTY_FILE_COPY_FOUND);
                        unlink($real_full_path);
                    } else {
                        Log::warn("  A file copy with a different file size than the original was found: $real_full_path is " . number_format($file_size) . " bytes. Original: $original_file_path is " . number_format($expected_file_size) . " bytes.", Log::EVENT_CODE_FSCK_SIZE_MISMATCH_FILE_COPY_FOUND);
                        Trash::trash_file($real_full_path);
                        $this->fsck_report->found_problem(FSCK_PROBLEM_WRONG_COPY_SIZE, array($file_size, $expected_file_size, $original_file_path), clean_dir($real_full_path));
                    }
                    unset($file_copies_inodes[$key]);
                    unset($file_metafiles[clean_dir($real_full_path)]);
                }
            }
        }
        if ($num_copies_required > 0 && $storage_path === FALSE && count($file_copies_inodes) == 0 && !isset($original_file_path)) {
            SambaSpool::parse_samba_spool();
            $query = "SELECT * FROM tasks WHERE action = 'rename' AND share = :share AND additional_info = :full_path";
            $task = DB::getFirst($query, array('share' => $share, 'full_path' => "$file_path/$filename"));
            if ($task) {
                Log::debug("  No file copies found, but is OK because this file was renamed after fsck started.");
                return;
            }
        }
        if (count($file_copies_inodes) == $num_copies_required) {
            if (!$found_linked_metafile || ($file_type != 'link' && $storage_path === FALSE)) {
                if (!$found_linked_metafile) {
                    Log::info('  Symlink target moved. Updating symlink.');
                    $this->fsck_report->found_problem(FSCK_COUNT_SYMLINK_TARGET_MOVED);
                } else {
                    Log::info('  Symlink was missing. Creating new symlink.');
                }
                foreach ($file_metafiles as $key => $metafile) {
                    if ($metafile->is_linked) {
                        $this->update_symlink($metafile->path, "$landing_zone/$file_path/$filename", $share, $file_path, $filename);
                        break;
                    }
                }
                Metastores::save_metafiles($share, $file_path, $filename, $file_metafiles);
            }
        } else if (count($file_copies_inodes) == 0 && !isset($original_file_path)) {
            if (!empty($file_path)) {
                $scanned_dir = dirname(clean_dir("$file_path/$filename"));
                SambaSpool::parse_samba_spool();
                $query = "SELECT * FROM tasks WHERE action = 'rename' AND share = :share";
                $tasks = DB::getAll($query, array('share' => $share));
                foreach ($tasks as $task) {
                    if (string_starts_with($scanned_dir, $task->additional_info)) {
                        Log::debug("  No file copies found, but is OK because this file is in a folder that was renamed after fsck started.");
                        return;
                    }
                }
            }
            $log_suffix = is_link("$landing_zone/$file_path/$filename") ? 'Deleting from share.' : (gh_is_file("$landing_zone/$file_path/$filename") ? 'Did you copy that file there without using your Samba shares? (If you did, don\'t do that in the future.)' : '');
            Log::warn('  WARNING! No copies of this file are available in the Greyhole storage pool: "' . clean_dir("$share/$file_path/$filename") . '". ' . $log_suffix, Log::EVENT_CODE_FSCK_NO_FILE_COPIES);
            if ($source == 'metastore' || Metastores::get_metafile_data_filename($share, $file_path, $filename) !== FALSE) {
                $this->fsck_report->found_problem(FSCK_PROBLEM_NO_COPIES_FOUND, clean_dir("$share/$file_path/$filename"));
            }
            if (is_link("$landing_zone/$file_path/$filename")) {
                $target = readlink("$landing_zone/$file_path/$filename");
                if (string_starts_with($target, '.')) {
                    Log::info("   Is OK... Is a symlink that starts with '.'. Will leave this alone.");
                } else {
                    Trash::trash_file("$landing_zone/$file_path/$filename");
                }
            } else if (gh_is_file("$landing_zone/$file_path/$filename")) {
                Log::info("$share/$file_path/$filename is a file (not a symlink). Adding a new 'write' pending task for that file.");
                SambaSpool::parse_samba_spool();
                WriteTask::queue($share, empty($file_path) ? $filename : clean_dir("$file_path/$filename"));
            }
            if ($this->has_option(OPTION_DEL_ORPHANED_METADATA)) {
                Metastores::remove_metafiles($share, $file_path, $filename);
            } else {
                Metastores::save_metafiles($share, $file_path, $filename, $file_metafiles);
            }
        } else if (count($file_copies_inodes) < $num_copies_required && $num_copies_required > 0) {
            Log::info("  Missing file copies. Expected $num_copies_required, got " . count($file_copies_inodes) . ". Will create more copies using $original_file_path");
            if ($this->fsck_report) {
                $this->fsck_report->found_problem(FSCK_COUNT_MISSING_COPIES);
            }
            clearstatcache(); $filesize = gh_filesize($original_file_path);
            $file_metafiles = Metastores::create_metafiles($share, "$file_path/$filename", $num_copies_required, $filesize, $file_metafiles);
            $symlink_created = FALSE;
            $num_copies_current = 1; # the source file
            global $going_drive;
            if (!empty($going_drive)) {
                $num_copies_current = 0;
            }
            foreach ($file_metafiles as $key => $metafile) {
                if ($original_file_path != $metafile->path) {
                    if ($num_copies_current >= $num_copies_required) {
                        $metafile->state = Metafile::STATE_GONE;
                        $file_metafiles[$key] = $metafile;
                        continue;
                    }
                    list($metafile_dir_path, ) = explode_full_path($metafile->path);
                    if ($metafile->state == Metafile::STATE_GONE) {
                        foreach (Config::storagePoolDrives() as $sp_drive) {
                            if (StoragePool::getDriveFromPath($metafile_dir_path) == $sp_drive && StoragePool::is_pool_drive($sp_drive)) {
                                $metafile->state = Metafile::STATE_PENDING;
                                $file_metafiles[$key] = $metafile;
                                break;
                            }
                        }
                    }
                    if ($metafile->state != Metafile::STATE_GONE) {
                        list($original_path, ) = explode_full_path(get_share_landing_zone($share) . "/$file_path");
                        if (!gh_mkdir($metafile_dir_path, $original_path)) {
                            $metafile->state = Metafile::STATE_GONE;
                            $file_metafiles[$key] = $metafile;
                            continue;
                        }
                    }
                    if (!is_dir($metafile_dir_path) || $metafile->state == Metafile::STATE_GONE) {
                        if ($metafile->state != Metafile::STATE_GONE) {
                            if (is_dir(normalize_utf8_characters($metafile_dir_path))) {
                                $metafile->path = normalize_utf8_characters($metafile->path);
                                $file_metafiles[$key] = $metafile;
                            } else {
                                continue;
                            }
                        }
                    }
                    if ($metafile->state == Metafile::STATE_PENDING) {
                        $expected_md5 = NULL;
                        if ($this->has_option(OPTION_VALIDATE_COPIES) && $num_retries <= 3) {
                            $q = "SELECT checksum FROM checksums WHERE share = :share AND full_path = :full_path";
                            $expected_md5 = DB::getFirstValue($q, ['share' => $share, 'full_path' => "$file_path/$filename"]);
                            if (!$expected_md5) {
                                if (empty($file_path)) {
                                    $expected_md5 = DB::getFirstValue($q, ['share' => $share, 'full_path' => $filename]);
                                }
                                if (!$expected_md5) {
                                    Log::debug("    MD5 not found in 'checksum' table ($share/$file_path/$filename). Will calculate it from the source file.");
                                    $output = exec("md5sum " . escapeshellarg($original_file_path));
                                    $output = explode(' ', $output);
                                    $expected_md5 = $output[0];
                                }
                            }
                        }
                        if (StorageFile::create_file_copy($original_file_path, $metafile->path, $expected_md5, $error)) {
                            $metafile->state = Metafile::STATE_OK;
                            $num_copies_current++;
                        } else {
                            if ($metafile->is_linked) {
                                $metafile->is_linked = FALSE;
                            }
                            $metafile->state = Metafile::STATE_GONE;
                        }
                        $file_metafiles[$key] = $metafile;
                    }
                }
                if ($original_file_path == $metafile->path || $metafile->is_linked) {
                    if (!empty($going_drive) && StoragePool::getDriveFromPath($metafile->path) == $going_drive) {
                        $metafile->is_linked = FALSE;
                        $metafile->state = Metafile::STATE_GONE;
                        $file_metafiles[$key] = $metafile;
                        continue;
                    }
                    if ($symlink_created /* already */) {
                        $metafile->is_linked = FALSE;
                        $file_metafiles[$key] = $metafile;
                        continue;
                    }
                    $this->update_symlink($metafile->path, "$landing_zone/$file_path/$filename", $share, $file_path, $filename);
                    $symlink_created = TRUE;
                }
            }
            if (!$symlink_created) {
                foreach ($file_metafiles as $key => $metafile) {
                    if ($metafile->state == Metafile::STATE_OK) {
                        $metafile->is_linked = TRUE;
                        $file_metafiles[$key] = $metafile;
                        $this->update_symlink($metafile->path, "$landing_zone/$file_path/$filename", $share, $file_path, $filename);
                        break;
                    }
                }
            }
            Metastores::save_metafiles($share, $file_path, $filename, $file_metafiles);
        } else {
            foreach ($file_copies_inodes as $inode => $meh) {
                if (string_starts_with($inode, '/')) {
                    unset($file_copies_inodes[$inode]);
                }
            }
            if (count($file_copies_inodes) > $num_copies_required) {
                Log::info("  Too many file copies. Expected $num_copies_required, got " . count($file_copies_inodes) . ". Will try to remove some.");
                if (DBSpool::isFileLocked($share, "$file_path/$filename") !== FALSE) {
                    Log::info("  File is locked. Will not remove copies at this time. The next fsck will try to remove copies again.");
                    return;
                }
                $this->fsck_report->found_problem(FSCK_COUNT_TOO_MANY_COPIES);
                $local_target_drives = array_values(StoragePool::choose_target_drives(0, TRUE, $share, $file_path));
                $gone_drives = array();
                foreach (Config::storagePoolDrives() as $sp_drive) {
                    $file = clean_dir("$sp_drive/$share/$file_path/$filename");
                    if (!array_contains($local_target_drives, $sp_drive) && file_exists($file)) {
                        $gone_drives[] = $sp_drive;
                        $local_target_drives[] = $sp_drive;
                    }
                }
                if (!empty($gone_drives)) {
                    Log::debug("Drives that shouldn't be used anymore: " . implode(' - ', $gone_drives));
                }
                while (count($file_copies_inodes) > $num_copies_required && !empty($local_target_drives)) {
                    $sp_drive = array_pop($local_target_drives);
                    $key = clean_dir("$sp_drive/$share/$file_path/$filename");
                    Log::debug("  Looking for copy at $key");
                    if (isset($file_metafiles[$key]) || gh_file_exists($key)) {
                        if (isset($file_metafiles[$key])) {
                            $metafile = $file_metafiles[$key];
                        }
                        if (gh_file_exists($key) || $metafile->state == Metafile::STATE_OK) {
                            Log::debug("    Found file copy at $key, or metadata file is marked OK.");
                            if (gh_is_file_locked($key) !== FALSE) {
                                Log::debug("    File copy is locked. Won't remove it.");
                                continue;
                            }
                            $this->fsck_report->found_problem(FSCK_PROBLEM_TOO_MANY_COPIES, $key);
                            Log::debug("    Removing copy at $key");
                            unset($file_copies_inodes[gh_fileinode($key)]);
                            Trash::trash_file($key);
                            if (isset($file_metafiles[$key])) {
                                unset($file_metafiles[$key]);
                            }
                            $num_ok--;
                        }
                    }
                }
                $found_linked_metafile = FALSE;
                foreach ($file_metafiles as $key => $metafile) {
                    if ($metafile->is_linked) {
                        $found_linked_metafile = ( @readlink("$landing_zone/$file_path/$filename") == $metafile->path );
                        break;
                    }
                }
                if (!$found_linked_metafile) {
                    $metafile = reset($file_metafiles);
                    $this->update_symlink($metafile->path, "$landing_zone/$file_path/$filename", $share, $file_path, $filename);
                    reset($file_metafiles)->is_linked = TRUE;
                }
                Metastores::save_metafiles($share, $file_path, $filename, $file_metafiles);
            }
        }
        if ($this->has_option(OPTION_CHECKSUMS)) {
            foreach (Metastores::get_metafiles($share, $file_path, $filename, TRUE) as $metafile_block) {
                foreach ($metafile_block as $metafile) {
                    if ($metafile->state != Metafile::STATE_OK) { continue; }
                    $inode_number = @gh_fileinode($metafile->path);
                    if ($inode_number !== FALSE) {
                        Md5Task::queue($share, clean_dir("$file_path/$filename"), $metafile->path);
                    }
                }
            }
        }
        if (!empty($error) && string_contains($error, 'MD5 mismatch')) {
            if ($num_retries <= 3) {
                if ($num_retries == 3) {
                    $this->fsck_report->found_problem(FSCK_PROBLEM_WRONG_MD5, $error, $original_file_path);
                }
                $this->gh_fsck_file($path, $filename, $file_type, $source, $share, $storage_path, $num_retries+1);
            }
        }
    }
    private function update_symlink($target, $symlink, $share, $file_path, $filename) {
        clearstatcache();
        if (!file_exists($symlink)) {
            Log::debug("  Missing symlink... A pending unlink transaction maybe?");
            SambaSpool::parse_samba_spool();
            $query = "SELECT * FROM tasks WHERE action = 'unlink' AND share = :share AND full_path = :full_path";
            $params = array(
                'share' => $share,
                'full_path' => trim("$file_path/$filename", '/'),
            );
            $task = DB::getFirst($query, $params);
            if ($task) {
                Log::debug("    Indeed! Pending unlink task found. Will not re-create this symlink.");
                return;
            }
            Log::debug("    No... Found no good reason for the symlink to be missing! Let's re-create it.");
        }
        Log::debug("  Updating symlink at $symlink to point to $target");
        Trash::trash_file($symlink);
        gh_mkdir(dirname($symlink), dirname($target));
        gh_symlink($target, $symlink);
    }
    public function set_fsck_report($fsck_report) {
        $this->fsck_report = $fsck_report;
    }
    public function initialize_fsck_report($what) {
        $this->fsck_report = new FSCKReport($what);
    }
    public function get_fsck_report() {
        return $this->fsck_report;
    }
}

class FsckFileTask extends FsckTask {
    public function execute() {
        $this->full_path = get_share_landing_zone($this->share) . '/' . $this->full_path;
        $file_type = @filetype($this->full_path);
        list($path, $filename) = explode_full_path($this->full_path);
        FSCKLogFile::loadFSCKReport('Missing files', $this); // Create or load the fsck_report from disk
        $this->gh_fsck_file($path, $filename, $file_type, 'metastore', $this->share);
        $send_email = $this->has_option(OPTION_EMAIL);
        if ($send_email || Hook::hasHookForEvent(LogHook::EVENT_TYPE_FSCK)) {
            FSCKLogFile::saveFSCKReport($send_email, $this);
        }
        return TRUE;
    }
    public static function queue($share, $full_path, $additional_info = NULL, $complete = 'idle') {
        parent::_queue('fsck_file', $share, $full_path, $additional_info, $complete);
    }
}

class Md5Task extends AbstractTask {
    public function execute() {
        static::gh_check_md5($this);
        return TRUE;
    }
    public static function gh_check_md5($task) {
        $share_options = SharesConfig::getConfigForShare($task->share);
        $query = "SELECT complete, COUNT(*) AS num, GROUP_CONCAT(id) AS ids FROM tasks WHERE action = 'md5' AND share = :share AND full_path = :full_path GROUP BY complete ORDER BY complete";
        $params = array(
            'share' => $task->share,
            'full_path' => $task->full_path
        );
        $rows = DB::getAll($query, $params);
        $complete_tasks = array_shift($rows); // ORDER BY complete ASC in the above query will always return complete='yes' first
        if (empty($complete_tasks)) {
            Log::debug("  Already checked this file. Skipping.");
            return;
        }
        $incomplete_tasks = $rows;
        if (count($incomplete_tasks) > 0) {
            $task->postpone();
            $num_worker_threads = (int) trim(exec("ps x | grep '/usr/bin/greyhole --md5-worker' | grep -v grep | grep -v bash | wc -l"));
            if ($num_worker_threads < count(Config::storagePoolDrives())) {
                Log::debug("  Will spawn new worker threads to work on this.");
                static::spawn_threads_for_pool_drives();
            } else {
                Log::debug("  Will wait some to allow for MD5 worker threads to complete.");
                sleep(5);
            }
            return;
        }
        Log::debug("Checking MD5 checksums for " . clean_dir("$task->share/$task->full_path"));
        $result_tasks = DB::getAll("SELECT * FROM tasks WHERE id IN ($complete_tasks->ids)");
        $md5s = array();
        foreach ($result_tasks as $t) {
            if (preg_match('/^(.+)=([0-9a-f]{32})$/', $t->additional_info, $regs)) {
                $md5s[$regs[2]][] = clean_dir($regs[1]);
            } else {
                $md5s['unreadable files'][] = clean_dir($t->additional_info);
            }
        }
        if (count($md5s) == 1) {
            $md5s = array_keys($md5s);
            $md5 = reset($md5s);
            if ($md5 == 'unreadable files') {
                $logs = array(
                    "  The following file is unreadable: " . clean_dir($t->additional_info),
                    "  The underlying filesystem probably contains errors. You should unmount that partition, and check it using e2fsck -cfp"
                );
            } else {
                log_file_checksum($task->share, $task->full_path, $md5);
                Log::debug("  All copies have the same MD5 checksum: $md5");
            }
        }
        else if (count($md5s) > 1) {
            $logs = array("Mismatch in file copies checksums:");
            foreach ($md5s as $md5 => $file_copies) {
                $latest_file_copy = $file_copies[count($file_copies)-1];
                $file_copies = array_unique($file_copies);
                sort($file_copies);
                $should_be_fixed = FALSE;
                $id = md5(clean_dir("$task->share/$task->full_path"));
                $q = "SELECT checksum FROM checksums WHERE id = :id";
                $original_md5 = DB::getFirstValue($q, array('id' => $id));
                if (!empty($original_md5) && $md5 != $original_md5) {
                    $q = "SELECT 1 FROM tasks WHERE share = :share AND full_path = :full_path AND action = 'write' LIMIT 1";
                    $queued_task = (bool) DB::getFirstValue($q, array('share' => $task->share, 'full_path' => $task->full_path));
                    if (!$queued_task) {
                        $q = "SELECT 1 FROM tasks WHERE share = :share AND additional_info = :full_path AND action = 'rename' LIMIT 1";
                        $queued_task = (bool) DB::getFirstValue($q, array('share' => $task->share, 'full_path' => $task->full_path));
                    }
                    $should_be_fixed = !$queued_task;
                    if ($should_be_fixed) {
                        unset($original_file_path);
                        foreach ($md5s as $_md5 => $_file_copies) {
                            if ($_md5 == $original_md5) {
                                $original_file_path = $_file_copies[count($_file_copies)-1];
                                break;
                            }
                        }
                        if (!isset($original_file_path)) {
                            $should_be_fixed = FALSE;
                        } else {
                            $lz_file_path = get_share_landing_zone($task->share) . "/" . $task->full_path;
                            if (clean_dir(readlink($lz_file_path)) == $latest_file_copy) {
                                unlink($lz_file_path);
                                symlink($original_file_path, $lz_file_path);
                            }
                        }
                    }
                }
                if (!$should_be_fixed) {
                    $original_file_path = clean_dir(readlink(get_share_landing_zone($task->share) . "/" . $task->full_path));
                    $should_be_fixed = ( count($md5s) == 2 && count($file_copies) == 1 && $latest_file_copy != $original_file_path );
                    if ($should_be_fixed) {
                        $original_md5 = 'Unknown';
                        foreach ($md5s as $this_md5 => $fcs) {
                            foreach ($fcs as $file_copy) {
                                if ($file_copy == $original_file_path) {
                                    $original_md5 = $this_md5;
                                    break;
                                }
                            }
                        }
                        if ($original_md5 == 'Unknown') {
                            Log::error("  The MD5 checksum of the original file ($original_file_path) was NOT calculated. Why?", Log::EVENT_CODE_FSCK_MD5_MISMATCH);
                            Log::info("  Calculating MD5 for original file copy at $original_file_path ...");
                            $original_md5 = md5_file($original_file_path);
                            Log::debug("    MD5 = $original_md5");
                        }
                    }
                }
                if ($should_be_fixed) {
                    Log::warn("  A file copy with a different checksum than the original was found: $latest_file_copy = $md5. Original: $original_file_path = $original_md5. This copy will be deleted, and replaced with a new copy from $original_file_path", Log::EVENT_CODE_FSCK_MD5_MISMATCH);
                    Trash::trash_file($latest_file_copy);
                    $metafiles = array();
                    list($path, $filename) = explode_full_path($task->full_path);
                    foreach (Metastores::get_metafiles($task->share, $path, $filename, TRUE, TRUE, FALSE) as $existing_metafiles) {
                        foreach ($existing_metafiles as $key => $metafile) {
                            if ($metafile->path == $latest_file_copy) {
                                $sp_drive = StoragePool::getDriveFromPath($latest_file_copy);
                                $df = StoragePool::get_free_space($sp_drive);
                                if (!$df) {
                                    $free_space = 0;
                                } else {
                                    $free_space = $df['free'];
                                }
                                $file_size = gh_filesize($latest_file_copy);
                                if ($free_space <= $file_size/1024) {
                                    Log::info("  Not enough free space left on $sp_drive. Will not re-create this file copy right now; will instead queue a fsck_file operation.");
                                    FsckFileTask::queue($task->share, $task->full_path);
                                    DBSpool::getInstance()->delete_tasks($complete_tasks->ids);
                                    return;
                                }
                            }
                            $metafiles[$key] = $metafile;
                        }
                    }
                    StorageFile::create_file_copies_from_metafiles($metafiles, $task->share, $task->full_path, $original_file_path, TRUE);
                    Log::debug("  Calculating MD5 for new file copy at $latest_file_copy ...");
                    $md5 = md5_file($latest_file_copy);
                    Log::debug("    MD5 = $md5");
                    if ($md5 == $original_md5) {
                        log_file_checksum($task->share, $task->full_path, $md5);
                        Log::debug("  This file copy now has the correct MD5 checksum: $md5");
                        DBSpool::getInstance()->delete_tasks($complete_tasks->ids);
                        foreach (Metastores::get_metafiles($task->share, $path, $filename, TRUE) as $metafile_block) {
                            foreach ($metafile_block as $metafile) {
                                if ($metafile->state != Metafile::STATE_OK) { continue; }
                                $inode_number = @gh_fileinode($metafile->path);
                                if ($inode_number !== FALSE) {
                                    Md5Task::queue($task->share, $task->full_path, $metafile->path);
                                }
                            }
                        }
                        return;
                    }
                }
                $files = implode(', ', $file_copies);
                $logs[] = "  [$md5] => $files";
            }
            $logs[] = "Some of the above files appear to be unreadable.";
            $logs[] = "The underlying filesystem(s) probably contains errors. You should unmount this/those partition(s), and check it/them using: fsck -cfp /dev/[...]";
            $logs[] = "You should manually check which file copy is invalid, and delete it. Re-create a valid copy with:";
            $logs[] = "  sudo greyhole --fsck --checksums --dir " . escapeshellarg(dirname(clean_dir($share_options[CONFIG_LANDING_ZONE] . "/$task->full_path")));
        }
        if (isset($logs)) {
            foreach ($logs as $log) {
                Log::error($log, Log::EVENT_CODE_FSCK_MD5_MISMATCH);
            }
            $flog = fopen(FSCKLogFile::PATH . '/fsck_checksums.log', 'a');
            if (!$flog) {
                Log::critical("Couldn't open log file: " . FSCKLogFile::PATH . "/fsck_checksums.log", Log::EVENT_CODE_FSCK_MD5_LOG_FAILURE);
            }
            fwrite($flog, $date = date("M d H:i:s") . ' ' . implode("\n", $logs) . "\n\n");
            fclose($flog);
            unset($logs);
        }
        DBSpool::getInstance()->delete_tasks($complete_tasks->ids);
    }
    public static function check_md5_workers() {
        $query = "SELECT * from tasks WHERE action = 'md5' AND complete = 'no' LIMIT 1";
        $row = DB::getFirst($query);
        if ($row) {
            $num_worker_threads = (int) trim(exec("ps x | grep '/usr/bin/greyhole --md5-worker' | grep -v grep | grep -v bash | wc -l"));
            if ($num_worker_threads == 0) {
                Log::debug("Will spawn new worker threads to work on incomplete checksums calculations.");
                static::spawn_threads_for_pool_drives();
            }
        }
    }
    public static function spawn_threads_for_pool_drives() {
        $checksums_thread_ids = array();
        foreach (Config::storagePoolDrives() as $sp_drive) {
            if (StoragePool::is_pool_drive($sp_drive)) {
                $already_running = (int) trim(exec("ps x | grep '/usr/bin/greyhole --md5-worker --drive=$sp_drive' | grep -v grep | grep -v bash | wc -l"));
                if ($already_running === 0) {
                    $checksums_thread_ids[] = spawn_thread('md5-worker', array($sp_drive));
                }
            }
        }
        return $checksums_thread_ids;
    }
    public static function queue($share, $full_path, $additional_info = NULL, $complete = 'no') {
        parent::_queue('md5', $share, $full_path, $additional_info, $complete);
    }
}

class MkdirTask extends AbstractTask {
    public function execute() {
    }
}

class RenameTask extends AbstractTask {
    private $fix_symlinks_scanned_dirs = [];
    public function execute() {
        $this->fix_symlinks_scanned_dirs = [];
        $share = $this->share;
        $full_path = $this->full_path;
        $target_full_path = $this->additional_info;
        $task_id = $this->id;
        $landing_zone = get_share_landing_zone($share);
        if (!$landing_zone) {
            return TRUE;
        }
        if ($this->should_ignore_file()) {
            return TRUE;
        }
        if (is_dir("$landing_zone/$target_full_path") || Metastores::dir_exists_in_metastores($share, $full_path)) {
            Log::info("Directory renamed: $landing_zone/$full_path -> $landing_zone/$target_full_path");
            foreach (Config::storagePoolDrives() as $sp_drive) {
                if (!StoragePool::is_pool_drive($sp_drive)) {
                    continue;
                }
                list($original_path, ) = explode_full_path(get_share_landing_zone($share) . "/$target_full_path");
                if (is_dir("$sp_drive/$share/$full_path")) {
                    list($path, ) = explode_full_path("$sp_drive/$share/$target_full_path");
                    gh_mkdir($path, $original_path);
                    gh_rename("$sp_drive/$share/$full_path", "$sp_drive/$share/$target_full_path");
                    $dir_permissions = StorageFile::get_file_permissions("$landing_zone/$target_full_path");
                    chown("$sp_drive/$share/$target_full_path", $dir_permissions->fileowner);
                    chgrp("$sp_drive/$share/$target_full_path", $dir_permissions->filegroup);
                    chmod("$sp_drive/$share/$target_full_path", $dir_permissions->fileperms);
                    Log::debug("  Directory moved: $sp_drive/$share/$full_path -> $sp_drive/$share/$target_full_path");
                }
                list($path, ) = explode_full_path("$sp_drive/" . Metastores::METASTORE_DIR . "/$share/$target_full_path");
                gh_mkdir($path, $original_path);
                $result = @gh_rename("$sp_drive/" . Metastores::METASTORE_DIR . "/$share/$full_path", "$sp_drive/" . Metastores::METASTORE_DIR . "/$share/$target_full_path");
                if ($result) {
                    Log::debug("  Metadata Store directory moved: $sp_drive/" . Metastores::METASTORE_DIR ."/$share/$full_path -> $sp_drive/" . Metastores::METASTORE_DIR . "/$share/$target_full_path");
                }
                $result = @gh_rename("$sp_drive/" . Metastores::METASTORE_BACKUP_DIR . "/$share/$full_path", "$sp_drive/" . Metastores::METASTORE_BACKUP_DIR . "/$share/$target_full_path");
                if ($result) {
                    Log::debug("  Backup Metadata Store directory moved: $sp_drive/" . Metastores::METASTORE_BACKUP_DIR . "/$share/$full_path -> $sp_drive/" . Metastores::METASTORE_BACKUP_DIR . "/$share/$target_full_path");
                }
            }
            exec('find ' . escapeshellarg("$landing_zone/$target_full_path") . ' -type f', $files_in_lz);
            foreach ($files_in_lz as $file_in_lz) {
                list($file_path, $filename) = explode_full_path($file_in_lz);
                FsckTask::getCurrentTask()->gh_fsck_file($file_path, $filename, 'file', 'landing_zone', $share);
            }
            foreach (Metastores::get_metafiles($share, $target_full_path, null, FALSE, FALSE, FALSE) as $existing_metafiles) {
                Log::debug("Existing metadata files: " . count($existing_metafiles));
                foreach ($existing_metafiles as $file_path => $file_metafiles) {
                    Log::debug("  File metafiles: " . count($file_metafiles));
                    $new_file_metafiles = array();
                    $symlinked = FALSE;
                    foreach ($file_metafiles as $key => $metafile) {
                        $old_path = $metafile->path;
                        $metafile->path = str_replace("/$share/$full_path/$file_path", "/$share/$target_full_path/$file_path", $metafile->path);
                        Log::debug("  Changing metadata file: $old_path -> $metafile->path");
                        $new_file_metafiles[$metafile->path] = $metafile;
                        if ($metafile->is_linked) {
                            $symlinked = TRUE;
                            $symlink_target = $metafile->path;
                        }
                    }
                    if (!$symlinked && count($file_metafiles) > 0) {
                        $metafile->is_linked = TRUE;
                        $file_metafiles[$key] = $metafile;
                        $symlink_target = $metafile->path;
                    }
                    if (is_link("$landing_zone/$target_full_path/$file_path") && !empty($symlink_target) && readlink("$landing_zone/$target_full_path/$file_path") != $symlink_target) {
                        Log::debug("  Updating symlink at $landing_zone/$target_full_path/$file_path to point to $symlink_target");
                        unlink("$landing_zone/$target_full_path/$file_path");
                        gh_symlink($symlink_target, "$landing_zone/$target_full_path/$file_path");
                    } else if (is_link("$landing_zone/$full_path/$file_path") && !empty($symlink_target) && !file_exists(readlink("$landing_zone/$full_path/$file_path"))) {
                        Log::debug("  Updating symlink at $landing_zone/$full_path/$file_path to point to $symlink_target");
                        unlink("$landing_zone/$full_path/$file_path");
                        gh_symlink($symlink_target, "$landing_zone/$full_path/$file_path");
                    } else {
                        $this->fix_symlinks($landing_zone, $share, "$full_path/$file_path", "$target_full_path/$file_path");
                    }
                    list($path, $filename) = explode_full_path("$target_full_path/$file_path");
                    Metastores::save_metafiles($share, $path, $filename, $new_file_metafiles);
                }
            }
        } else {
            Log::info("File renamed: $landing_zone/$full_path -> $landing_zone/$target_full_path");
            if ($this->is_file_locked($share, $target_full_path)) {
                return FALSE;
            }
            list($path, $filename) = explode_full_path($full_path);
            list($target_path, $target_filename) = explode_full_path($target_full_path);
            foreach (Metastores::get_metafiles($share, $path, $filename, FALSE, FALSE, FALSE) as $existing_metafiles) {
                if (file_exists("$landing_zone/$target_full_path") && (count($existing_metafiles) > 0 || !is_link("$landing_zone/$target_full_path"))) {
                    foreach (Metastores::get_metafiles($share, $target_path, $target_filename, TRUE, FALSE, FALSE) as $existing_target_metafiles) {
                        if (count($existing_target_metafiles) > 0) {
                            foreach ($existing_target_metafiles as $metafile) {
                                Trash::trash_file($metafile->path);
                            }
                            Metastores::remove_metafiles($share, $target_path, $target_filename);
                        }
                    }
                }
                if (count($existing_metafiles) == 0) {
                    foreach (Metastores::get_metafiles($share, $path, $filename, TRUE, FALSE, FALSE) as $all_existing_metafiles) {
                        if (count($all_existing_metafiles) > 0) {
                            Metastores::remove_metafiles($share, $path, $filename);
                        }
                    }
                    AbstractTask::instantiate(['id' => $task_id, 'action' => 'write', 'share' => $share, 'full_path' => $target_full_path, 'complete' => 'yes'])->execute();
                } else {
                    $symlinked = FALSE;
                    foreach ($existing_metafiles as $key => $metafile) {
                        $old_path = $metafile->path;
                        $metafile->path = str_replace("/$share/$full_path", "/$share/$target_full_path", $old_path);
                        Log::debug("  Renaming copy at $old_path to $metafile->path");
                        list($metafile_dir_path, ) = explode_full_path($metafile->path);
                        list($original_path, ) = explode_full_path(get_share_landing_zone($share) . "/$target_full_path");
                        gh_mkdir($metafile_dir_path, $original_path);
                        $it_worked = gh_rename($old_path, $metafile->path);
                        if ($it_worked) {
                            if ($metafile->is_linked) {
                                $symlinked = TRUE;
                                $symlink_target = $metafile->path;
                            }
                        } else {
                            Log::warn("    Warning! An error occurred while renaming file copy $old_path to $metafile->path.", Log::EVENT_CODE_RENAME_FILE_COPY_FAILED);
                        }
                        $existing_metafiles[$key] = $metafile;
                    }
                    if (!$symlinked && count($existing_metafiles) > 0) {
                        $metafile->is_linked = TRUE;
                        $existing_metafiles[$key] = $metafile;
                        $symlink_target = $metafile->path;
                    }
                    Metastores::remove_metafiles($share, $path, $filename);
                    Metastores::save_metafiles($share, $target_path, $target_filename, $existing_metafiles);
                    if (is_link("$landing_zone/$target_full_path")) {
                        if (readlink("$landing_zone/$target_full_path") != $symlink_target) {
                            Log::debug("  Updating symlink at $landing_zone/$target_full_path to point to $symlink_target");
                            unlink("$landing_zone/$target_full_path");
                            gh_symlink($symlink_target, "$landing_zone/$target_full_path");
                        }
                    } else if (is_link("$landing_zone/$full_path") && !file_exists(readlink("$landing_zone/$full_path"))) {
                        Log::debug("  Updating symlink at $landing_zone/$full_path to point to $symlink_target");
                        unlink("$landing_zone/$full_path");
                        gh_symlink($symlink_target, "$landing_zone/$full_path");
                    } else {
                        $this->fix_symlinks($landing_zone, $share, $full_path, $target_full_path);
                    }
                }
            }
        }
        DBSpool::resetSleepingTasks();
        FileHook::trigger(FileHook::EVENT_TYPE_RENAME, $share, $target_full_path, $full_path);
        return TRUE;
    }
    public function should_ignore_file($share = NULL, $full_path = NULL) {
        if (empty($full_path)) {
            $full_path = $this->additional_info;
        }
        return parent::should_ignore_file($share, $full_path);
    }
    private function fix_symlinks($landing_zone, $share, $full_path, $target_full_path) {
        if (isset($this->fix_symlinks_scanned_dirs[$landing_zone])) {
            return;
        }
        Log::info("  Scanning $landing_zone for broken links... This can take a while!");
        exec("find -L " . escapeshellarg($landing_zone) . " -type l", $broken_links);
        Log::debug("    Found " . count($broken_links) . " broken links.");
        foreach ($broken_links as $broken_link) {
            $fixed_link_target = readlink($broken_link);
            Log::debug("    Found a broken symlink to update: $broken_link. Broken target: $fixed_link_target");
            foreach (Config::storagePoolDrives() as $sp_drive) {
                $fixed_link_target = str_replace(clean_dir("$sp_drive/$share/$full_path/"), clean_dir("$sp_drive/$share/$target_full_path/"), $fixed_link_target);
                if ($fixed_link_target == "$sp_drive/$share/$full_path") {
                    $fixed_link_target = "$sp_drive/$share/$target_full_path";
                    break;
                }
            }
            if (gh_is_file($fixed_link_target)) {
                Log::debug("      New (fixed) target: $fixed_link_target");
                unlink($broken_link);
                gh_symlink($fixed_link_target, $broken_link);
            }
        }
        $this->fix_symlinks_scanned_dirs[$landing_zone] = TRUE;
    }
}

define('OPTION_DRIVE_IS_AVAILABLE', 'drive-is-avail');
class RemoveTask extends AbstractTask {
    private function isGoing() {
        return $this->has_option(OPTION_DRIVE_IS_AVAILABLE);
    }
    protected $drive;
    protected $log = '';
    public function execute() {
        $this->drive = $this->full_path;
        StoragePool::remove_drive($this->drive);
        if ($this->isGoing()) {
            file_put_contents($this->drive . "/.greyhole_used_this", "Flag to prevent Greyhole from thinking this drive disappeared for no reason...");
            Metastores::choose_metastores_backups();
            Log::info("Storage pool drive " . $this->drive . " will be removed from the storage pool.");
            $this->log("Storage pool drive " . $this->drive . " will be removed from the storage pool.\n");
            global $going_drive; // Used in StoragePool::is_pool_drive()
            $going_drive = $this->drive;
            $fsck_task = FsckTask::getCurrentTask(array('additional_info' => OPTION_ORPHANED . '|' . OPTION_DU . '|' . OPTION_VALIDATE_COPIES));
            foreach (SharesConfig::getShares() as $share_name => $share_options) {
                if (!is_dir("$going_drive/$share_name")) {
                    $this->log("Share '$share_name' not found on $going_drive... Skipping.");
                    continue;
                }
                $this->log();
                if ($share_options[CONFIG_NUM_COPIES] == 1) {
                    $this->log("Moving file copies for share '$share_name'... Please be patient... ");
                    if (is_dir("$going_drive/$share_name")) {
                        $fsck_task->initialize_fsck_report("Removing drive $going_drive; share with only 1 copy: $share_name");
                        $fsck_task->gh_fsck_reset_du($share_name);
                        $fsck_task->gh_fsck($share_options[CONFIG_LANDING_ZONE], $share_name);
                        $errors = @$fsck_task->get_fsck_report()->found_problems[FSCK_PROBLEM_WRONG_MD5];
                        if (is_array($errors)) {
                            foreach ($errors as $file_path => $error) {
                                $this->log("  Failed to create copy of $file_path: $error");
                            }
                        }
                    }
                } else {
                    @rename("$going_drive/$share_name", "$going_drive/$share_name".".tmp");
                    fix_symlinks_on_share($share_name);
                    @rename("$going_drive/$share_name".".tmp", "$going_drive/$share_name");
                    $this->log("Checking that all the files in the share '$share_name' also exist on another drive...");
                    static::check_going_dir("$going_drive/$share_name", $share_name, $going_drive);
                }
                $this->log("  Done.");
            }
        }
        ConfigHelper::removeStoragePoolDrive($this->drive);
        $this->log();
        if (SystemHelper::is_amahi()) {
            $this->log("You should de-select this partition in your Amahi dashboard (http://hda), in the Shares > Storage Pool page.");
        }
        StoragePool::mark_gone_ok($this->drive, 'remove');
        StoragePool::mark_gone_drive_fscked($this->drive, 'remove');
        Log::info("Storage pool drive $this->drive has been removed.");
        $this->log("Storage pool drive $this->drive has been removed from your pool, which means the missing file copies that are in this drive will be re-created during the next fsck.");
        DBSpool::archive_task($this->id);
        if ($this->isGoing()) {
            schedule_fsck_all_shares(array('email'));
            $this->log("All the files that were only on $this->drive have been copied somewhere else.");
            $this->log("A fsck of all shares has been scheduled, to recreate other file copies. It will start after all currently pending tasks have been completed.");
            unlink($this->drive . "/.greyhole_used_this");
        } else {
            $this->log("Sadly, file copies that were only on this drive, if any, are now lost!");
        }
        $subject = "[Greyhole] Removal of pool drive at $this->drive completed on " . exec('hostname');
        email_sysadmin($subject, $this->log);
        DaemonRunner::restart_service();
    }
    protected function log($log = '') {
        $this->log .= "$log\n";
    }
    protected static function check_going_dir($path, $share, $going_drive) {
        $handle = @opendir($path);
        if ($handle === FALSE) {
            Log::error("Couldn't open $path to list content. Skipping...", Log::EVENT_CODE_LIST_DIR_FAILED);
            return;
        }
        Log::debug("Entering $path");
        while (($filename = readdir($handle)) !== FALSE) {
            if ($filename == '.' || $filename == '..') { continue; }
            $full_path = "$path/$filename";
            $file_type = @filetype($full_path);
            if ($file_type === FALSE) {
                $file_type = @filetype(normalize_utf8_characters($full_path));
                if ($file_type !== FALSE) {
                    $full_path = normalize_utf8_characters($full_path);
                    $path = normalize_utf8_characters($path);
                }
            }
            if ($file_type == 'dir') {
                static::check_going_dir($full_path, $share, $going_drive);
            } else {
                $file_path = trim(mb_substr($path, mb_strlen("$going_drive/$share")+1), '/');
                $file_metafiles = array();
                $file_copies_inodes = StoragePool::get_file_copies_inodes($share, $file_path, $filename, $file_metafiles, TRUE);
                if (count($file_copies_inodes) == 0) {
                    Log::debug("Found a file, $full_path, that has no other copies on other drives. Removing $going_drive would make that file disappear! Will create extra copies now.");
                    FsckTask::getCurrentTask()->gh_fsck_file($path, $filename, $file_type, 'landing_zone', $share, $going_drive);
                }
            }
        }
        closedir($handle);
    }
}

class RemoveShareTask extends AbstractTask {
    protected $log = '';
    public function execute() {
        $landing_zone = get_share_landing_zone($this->share);
        $log = "Will remove '$this->share' share from the Greyhole storage pool, by moving all the data files inside this share to it's landing zone: $landing_zone";
        $this->log($log);
        Log::info($log);
        $storage_pool_drives_to_unwind = array();
        foreach (Config::storagePoolDrives() as $sp_drive) {
            if (!file_exists("$sp_drive/$this->share")) {
                continue;
            }
            if (SystemHelper::directory_uuid("$sp_drive/$this->share") === SystemHelper::directory_uuid("$landing_zone")) {
                array_unshift($storage_pool_drives_to_unwind, $sp_drive);
            } else {
                array_push($storage_pool_drives_to_unwind, $sp_drive);
            }
        }
        $this->log();
        $log = "Deleting symlinks from landing zone... This might take a while.";
        $this->log($log);
        Log::debug($log);
        exec("find " . escapeshellarg($landing_zone) . " -type l -delete");
        $this->log("  Done.");
        $num_files_total = 0;
        foreach ($storage_pool_drives_to_unwind as $sp_drive) {
            unset($result);
            $log = "Moving files from $sp_drive/$this->share into $landing_zone... This might take a while.";
            $this->log($log);
            Log::debug($log);
            exec("rsync -rlptgoDuvW --remove-source-files " . escapeshellarg("$sp_drive/$this->share/") . " " . escapeshellarg('/' . trim($landing_zone, '/')), $result);
            $num_files = count($result)-5;
            if ($num_files < 0) {
                $num_files = 0;
            }
            $num_files_total += $num_files;
            exec("find " . escapeshellarg("$sp_drive/$this->share/") . " -type d -delete");
            $this->log("  Done. Copied $num_files files.");
        }
        $this->log("All done. Copied $num_files_total files.\n\nYou should now remove the Greyhole options from the [$this->share] share in your smb.conf.");
        ConfigHelper::removeShare($this->share);
        DBSpool::archive_task($this->id);
        Log::info("Share removal completed.");
        $this->sendEmail();
        DaemonRunner::restart_service();
        return TRUE;
    }
    protected function sendEmail() {
        $subject = "[Greyhole] Removal of share '$this->share' completed on " . exec('hostname');
        email_sysadmin($subject, $this->log);
    }
    protected function log($log = '') {
        $this->log .= "$log\n";
    }
}

class RmdirTask extends AbstractTask {
    public function execute() {
        $share = $this->share;
        $full_path = $this->full_path;
        $landing_zone = get_share_landing_zone($share);
        if (!$landing_zone) {
            return TRUE;
        }
        Log::info("Directory deleted: $landing_zone/$full_path");
        if (array_contains(ConfigHelper::$trash_share_names, $share)) {
            foreach (Config::storagePoolDrives() as $sp_drive) {
                if (@rmdir("$sp_drive/.gh_trash/$full_path")) {
                    Log::debug("  Removed copy from trash at $sp_drive/.gh_trash/$full_path");
                }
            }
            return TRUE;
        }
        foreach (Config::storagePoolDrives() as $sp_drive) {
            if (@rmdir("$sp_drive/$share/$full_path/")) {
                Log::debug("  Removed copy at $sp_drive/$share/$full_path");
            }
            $metastore = "$sp_drive/" . Metastores::METASTORE_DIR;
            if (@rmdir("$metastore/$share/$full_path/")) {
                Log::debug("  Removed metadata files directory $metastore/$share/$full_path");
            }
        }
        FileHook::trigger(FileHook::EVENT_TYPE_RMDIR, $share, $full_path);
        return TRUE;
    }
}

class UnlinkTask extends AbstractTask {
    public function execute() {
        $share = $this->share;
        $full_path = $this->full_path;
        $landing_zone = get_share_landing_zone($share);
        if (!$landing_zone) {
            return TRUE;
        }
        if ($this->should_ignore_file()) {
            return TRUE;
        }
        Log::info("File deleted: $landing_zone/$full_path");
        if (array_contains(ConfigHelper::$trash_share_names, $share)) {
            $full_path = preg_replace('/ copy [0-9]+$/', '', $full_path);
            Log::debug("  Looking for corresponding file in trash to delete...");
            foreach (Config::storagePoolDrives() as $sp_drive) {
                if (file_exists("$sp_drive/.gh_trash/$full_path")) {
                    $delete = TRUE;
                    $trash_share = SharesConfig::getConfigForShare(CONFIG_TRASH_SHARE);
                    if ($trash_share) {
                        list($path, ) = explode_full_path("{$trash_share[CONFIG_LANDING_ZONE]}/$full_path");
                        if ($dh = @opendir($path)) {
                            while (($file = readdir($dh)) !== FALSE) {
                                if ($file == '.' || $file == '..') { continue; }
                                if (is_link("$path/$file") && readlink("$path/$file") == "$sp_drive/.gh_trash/$full_path") {
                                    $delete = FALSE;
                                    continue;
                                }
                            }
                        }
                    }
                    if ($delete) {
                        Log::debug("    Deleting corresponding copy $sp_drive/.gh_trash/$full_path");
                        unlink("$sp_drive/.gh_trash/$full_path");
                        break;
                    }
                }
            }
            return TRUE;
        }
        if (gh_file_exists("$landing_zone/$full_path") && !is_dir("$landing_zone/$full_path")) {
            Log::debug("  File still exists in landing zone; a new file replaced the one deleted here. Skipping.");
            return TRUE;
        }
        list($path, $filename) = explode_full_path($full_path);
        foreach (Metastores::get_metafiles($share, $path, $filename, TRUE) as $existing_metafiles) {
            foreach ($existing_metafiles as $metafile) {
                Trash::trash_file($metafile->path);
            }
        }
        Metastores::remove_metafiles($share, $path, $filename);
        FileHook::trigger(FileHook::EVENT_TYPE_DELETE, $share, $full_path);
        return TRUE;
    }
}

class WriteTask extends AbstractTask {
    public function execute() {
        $share = $this->share;
        $full_path = $this->full_path;
        $task_id = $this->id;
        $landing_zone = get_share_landing_zone($share);
        if (!$landing_zone) {
            return TRUE;
        }
        if (string_starts_with($this->additional_info, 'source:')) {
            $source_file = substr($this->additional_info, 7);
        } elseif ($this->should_ignore_file()) {
            return TRUE;
        }
        if (!gh_file_exists(!empty($source_file) ? $source_file : "$landing_zone/$full_path", '$real_path doesn\'t exist anymore.')) {
            $new_full_path = static::find_future_full_path($share, $full_path, $task_id);
            if ($new_full_path != $full_path && gh_is_file("$landing_zone/$new_full_path")) {
                Log::debug("  Found that $full_path has been renamed to $new_full_path. Will work using that instead.");
                if (is_link("$landing_zone/$new_full_path")) {
                    $source_file = clean_dir(readlink("$landing_zone/$new_full_path"));
                } else {
                    $source_file = clean_dir("$landing_zone/$new_full_path");
                }
            } else {
                Log::info("  Skipping.");
                if (!gh_file_exists($landing_zone, '  Share "' . $share . '" landing zone "$real_path" doesn\'t exist anymore. Will not process this task until it re-appears...')) {
                    DBSpool::getInstance()->postpone_task($task_id);
                }
                return TRUE;
            }
        }
        $num_copies_required = SharesConfig::getNumCopies($share);
        if ($num_copies_required === -1) {
            return TRUE;
        }
        list($path, $filename) = explode_full_path($full_path);
        if ((isset($new_full_path) && is_link("$landing_zone/$new_full_path")) || is_link("$landing_zone/$full_path")) {
            if (!isset($source_file)) {
                $source_file = clean_dir(readlink("$landing_zone/$full_path"));
            }
            clearstatcache();
            $filesize = gh_filesize($source_file);
            if (Log::getLevel() >= Log::DEBUG) {
                Log::info("File changed: $share/$full_path - " . bytes_to_human($filesize, FALSE));
            } else {
                Log::info("File changed: $share/$full_path");
            }
            Log::debug("  Will use source file: $source_file");
            foreach (Metastores::get_metafiles($share, $path, $filename, TRUE) as $existing_metafiles) {
                $keys_to_remove = array();
                $found_source_file = FALSE;
                foreach ($existing_metafiles as $key => $metafile) {
                    $metafile->path = clean_dir($metafile->path);
                    if ($metafile->path == $source_file) {
                        $metafile->is_linked = TRUE;
                        $metafile->state = Metafile::STATE_OK;
                        $found_source_file = TRUE;
                    } else {
                        Log::debug("  Will remove copy at $metafile->path");
                        $keys_to_remove[] = $metafile->path;
                    }
                }
                if (!$found_source_file && !empty($keys_to_remove)) {
                    $key = array_shift($keys_to_remove);
                    $source_file = $existing_metafiles[$key]->path;
                    Log::debug("  Change of mind... Will use source file: $source_file");
                }
                $new_metafiles = $this->gh_write_process_metafiles($num_copies_required, $existing_metafiles, $share, $full_path, $source_file, $filesize, $task_id, $keys_to_remove);
                if ($new_metafiles === FALSE || $new_metafiles === TRUE) {
                    return $new_metafiles;
                }
                if (!empty($new_metafiles) && !isset($new_metafiles[$source_file])) {
                    Log::debug("  Source file is not needed anymore. Deleting...");
                    $is_ok = FALSE;
                    foreach ($new_metafiles as $metafile) {
                        if ($metafile->state == Metafile::STATE_OK) {
                            $is_ok = TRUE;
                            break;
                        }
                    }
                    if ($is_ok) {
                        Trash::trash_file($source_file, TRUE);
                    } else {
                        Log::debug("  Change of mind... Couldn't find any OK metadata file... Will keep the source!");
                    }
                }
            }
            FileHook::trigger(FileHook::EVENT_TYPE_EDIT, $share, $full_path);
        } else {
            if (!isset($source_file)) {
                $source_file = clean_dir("$landing_zone/$full_path");
            }
            clearstatcache();
            $filesize = gh_filesize($source_file);
            if (Log::getLevel() >= Log::DEBUG) {
                Log::info("File created: $share/$full_path - " . bytes_to_human($filesize, FALSE));
            } else {
                Log::info("File created: $share/$full_path");
            }
            if (is_dir($source_file)) {
                Log::info("$share/$full_path is now a directory! Aborting.");
                return TRUE;
            }
            foreach (Metastores::get_metafiles($share, $path, $filename) as $existing_metafiles) {
                if (!empty($existing_metafiles)) {
                    foreach ($existing_metafiles as $metafile) {
                        Trash::trash_file($metafile->path);
                    }
                    Metastores::remove_metafiles($share, $path, $filename);
                    $existing_metafiles = array();
                    foreach (Config::storagePoolDrives() as $sp_drive) {
                        if (file_exists("$sp_drive/$share/$path/$filename")) {
                            Trash::trash_file("$sp_drive/$share/$path/$filename");
                        }
                    }
                }
                $new_metafiles = $this->gh_write_process_metafiles($num_copies_required, $existing_metafiles, $share, $full_path, $source_file, $filesize, $task_id);
                if ($new_metafiles === FALSE || $new_metafiles === TRUE) {
                    return $new_metafiles;
                }
            }
            FileHook::trigger(FileHook::EVENT_TYPE_CREATE, $share, $full_path);
        }
        return TRUE;
    }
    private function gh_write_process_metafiles($num_copies_required, $existing_metafiles, $share, $full_path, $source_file, $filesize, $task_id, $keys_to_remove=NULL) {
        $landing_zone = get_share_landing_zone($share);
        list($path, $filename) = explode_full_path($full_path);
        if ($num_copies_required > 1 || empty($existing_metafiles)) {
            if ($this->is_file_locked($share, $full_path)) {
                return FALSE;
            }
            DBSpool::resetSleepingTasks();
        }
        if ($keys_to_remove !== NULL) {
            foreach ($keys_to_remove as $key) {
                if ($existing_metafiles[$key]->path != $source_file) {
                    Trash::trash_file($existing_metafiles[$key]->path, TRUE);
                }
            }
            $existing_metafiles = array();
        }
        $metafiles = Metastores::create_metafiles($share, $full_path, $num_copies_required, $filesize, $existing_metafiles);
        if (count($metafiles) == 0) {
            Log::error("  No metadata files could be created. Will wait until metadata files can be created to work on this file.", Log::EVENT_CODE_NO_METADATA_SAVED);
            DBSpool::getInstance()->postpone_task($task_id);
            return array();
        }
        if (!is_link("$landing_zone/$full_path")) {
            $i = 0;
            foreach ($metafiles as $metafile) {
                $metafile->is_linked = ($i++ == 0);
            }
        }
        Metastores::save_metafiles($share, $path, $filename, $metafiles);
        $q = "SELECT id FROM tasks WHERE action = 'write' AND share = :share AND full_path = :full_path AND complete IN ('yes', 'thawed', 'idle') AND id > :task_id";
        $duplicate_tasks_to_delete = DB::getAllValues($q, ['share' => $share, 'full_path' => $full_path, 'task_id' => $task_id]);
        if (!StorageFile::create_file_copies_from_metafiles($metafiles, $share, $full_path, $source_file)) {
            return TRUE;
        }
        if (!empty($duplicate_tasks_to_delete)) {
            Log::debug("  Deleting " . count($duplicate_tasks_to_delete) . " future 'write' tasks that are duplicate of this one.");
            DBSpool::getInstance()->delete_tasks($duplicate_tasks_to_delete);
        }
        return $metafiles;
    }
    public static function queue($share, $full_path, $complete = 'yes') {
        parent::_queue('write', $share, $full_path, NULL, $complete);
    }
    private static function find_future_full_path($share, $full_path, $task_id) {
        $new_full_path = $full_path;
        while ($next_task = DBSpool::getInstance()->find_next_rename_task($share, $new_full_path, $task_id)) {
            if ($next_task->full_path == $full_path) {
                $new_full_path = $next_task->additional_info;
            } else {
                $new_full_path = preg_replace("@^$next_task->full_path@", $next_task->additional_info, $new_full_path);
            }
            $task_id = $next_task->id;
        }
        return $new_full_path;
    }
}

class LinkTask extends WriteTask {
    public function execute() {
        $share = $this->share;
        $full_path = $this->full_path;
        $target_full_path = $this->additional_info;
        $landing_zone = get_share_landing_zone($share);
        if (!$landing_zone) {
            return TRUE;
        }
        if ($this->should_ignore_file()) {
            return TRUE;
        }
        Log::info("File (hard)linked: $landing_zone/$target_full_path -> $landing_zone/$full_path");
        $this->full_path = $target_full_path;
        parent::execute();
        return TRUE;
    }
    public function should_ignore_file($share = NULL, $full_path = NULL) {
        if (empty($full_path)) {
            $full_path = $this->additional_info;
        }
        return parent::should_ignore_file($share, $full_path);
    }
}

abstract class AbstractTask {
    public $id;
    public $action;
    public $share;
    public $full_path;
    public $additional_info;
    public $complete;
    public $event_date;
    public static function instantiate($task) {
        $task = to_object($task);
        $class_name = str_replace(' ', '', ucwords(str_replace(['_', '-'], ' ', $task->action))) . 'Task';
        return new $class_name($task);
    }
    public function __construct($task) {
        $task = to_object($task);
        $this->id              = @$task->id;
        $this->action          = @$task->action;
        $this->share           = @$task->share;
        $this->full_path       = @$task->full_path;
        $this->additional_info = @$task->additional_info;
        $this->complete        = @$task->complete;
        $this->event_date      = @$task->event_date;
    }
    public function shouldBeFrozen() {
        if ($this->complete != 'thawed') {
            $frozen_directories = Config::get(CONFIG_FROZEN_DIRECTORY);
            foreach ($frozen_directories as $frozen_directory) {
                if (string_starts_with("$this->share/$this->full_path", $frozen_directory)) {
                    return TRUE;
                }
            }
        }
        return FALSE;
    }
    public function has_option($option) {
        return string_contains($this->additional_info, $option);
    }
    abstract function execute();
    protected static function _queue($action, $share, $full_path, $additional_info, $complete) {
        $query = "INSERT INTO tasks SET action = :action, share = :share, full_path = :full_path, complete = :complete, additional_info = :additional_info";
        $params = array(
            'action'          => $action,
            'share'           => $share,
            'full_path'       => $full_path,
            'additional_info' => $additional_info,
            'complete'        => $complete,
        );
        DB::insert($query, $params);
    }
    public function postpone() {
        $query = "INSERT INTO tasks (action, share, full_path, additional_info, complete) SELECT action, share, full_path, additional_info, complete FROM tasks WHERE id = :task_id";
        DB::insert($query, array('task_id' => $this->id));
    }
    public function should_ignore_file($share = NULL, $full_path = NULL) {
        if (empty($share)) {
            $share = $this->share;
        }
        if (empty($full_path)) {
            $full_path = $this->full_path;
        }
        list($path, $filename) = explode_full_path($full_path);
        foreach (Config::get(CONFIG_IGNORED_FILES) as $ignored_file_re) {
            if (preg_match(';^' . $ignored_file_re . '$;', $filename)) {
                Log::info("Ignoring task because it matches the following '" . CONFIG_IGNORED_FILES . "' pattern: $ignored_file_re");
                return TRUE;
            }
        }
        foreach (Config::get(CONFIG_IGNORED_FOLDERS) as $ignored_folder_re) {
            $p = clean_dir("$share/$path/");
            if (preg_match(';^' . $ignored_folder_re . '$;', $p)) {
                Log::info("Ignoring task because it matches the following '" . CONFIG_IGNORED_FOLDERS . "' pattern: $ignored_folder_re");
                return TRUE;
            }
        }
        return FALSE;
    }
    protected function is_file_locked($share, $full_path) {
        $locked_by = DBSpool::isFileLocked($share, $full_path);
        if ($locked_by !== FALSE) {
            Log::debug("  File $share/$full_path is locked by another process ($locked_by). Will wait until it's unlocked to work on any file in this share.");
            DBSpool::lockShare($share, $full_path, $this->id);
            return TRUE;
        }
        return FALSE;
    }
}

final class DBSpool {
    private static $_instance;
    private $use_old_vfs = FALSE;
    private $current_task = NULL;
    private $locked_shares = array();
    private $sleep_before_task = array();
    private $next_tasks = array();
    private $locked_files = array();
    private $written_files = array();
    public static function getInstance() {
        if (empty(static::$_instance)) {
            static::$_instance = new self();
        }
        return static::$_instance;
    }
    public function __construct() {
        $arch = exec('uname -m');
        if (stripos($arch, 'armv5') !== FALSE) {
            $this->use_old_vfs = TRUE;
        }
    }
    public function fetch_next_tasks($incl_md5, $update_idle, $include_written = TRUE) {
        $where_clause = "";
        if (!empty($this->locked_shares)) {
            $where_clause .= " AND share NOT IN ('" . implode("','", array_keys($this->locked_shares)) . "')";
        }
        if (!$incl_md5) {
            $where_clause .= " AND action != 'md5'";
        }
        if ($include_written) {
            $statuses = "'yes', 'thawed', 'written'";
        } else {
            $statuses = "'yes', 'thawed'";
        }
        $query = "SELECT id, action, share, full_path, additional_info, complete FROM tasks WHERE complete IN ($statuses) $where_clause ORDER BY id ASC LIMIT 20";
        $tasks = DB::getAll($query);
        if (empty($tasks) && $update_idle) {
            $query = "UPDATE tasks SET complete = 'yes' WHERE complete = 'idle'";
            DB::execute($query);
            $tasks = $this->fetch_next_tasks($incl_md5, FALSE);
        }
        return $tasks;
    }
    public static function getCurrentTask() {
        return static::getInstance()->current_task;
    }
    public static function isCurrentTaskRetry() {
        $current_task = static::getCurrentTask();
        return !empty($current_task) && $current_task->id === 0;
    }
    public static function lockShare($share, $full_path, $task_id) {
        static::getInstance()->locked_shares[$share] = TRUE;
        if (!empty($full_path) && !empty($task_id)) {
            $q = "SELECT id FROM tasks WHERE action = 'write' AND share = :share AND full_path = :full_path AND complete IN ('yes', 'thawed', 'idle') AND id > :task_id";
            $duplicate_tasks_to_delete = DB::getAllValues($q, ['share' => $share, 'full_path' => $full_path, 'task_id' => $task_id]);
            if (!empty($duplicate_tasks_to_delete)) {
                Log::debug("  Deleting " . count($duplicate_tasks_to_delete) . " future 'write' tasks that are duplicate of this one.");
                DBSpool::getInstance()->delete_tasks($duplicate_tasks_to_delete);
            }
        }
    }
    public static function resetSleepingTasks() {
        static::getInstance()->sleep_before_task = array();
    }
    public static function setNextTask($task) {
        array_unshift(static::getInstance()->next_tasks, $task);
    }
    private function lockFile($idx, $locked_by) {
        $this->locked_files[$idx] = $locked_by;
    }
    public static function isFileLocked($share, $full_path) {
        $db_spool = static::getInstance();
        $idx = clean_dir("$share/$full_path");
        if (isset($db_spool->locked_files[$idx])) {
            return $db_spool->locked_files[$idx];
        }
        if (Config::get(CONFIG_CHECK_FOR_OPEN_FILES) === FALSE) {
            Log::debug("  Skipping open file (lock) check.");
            return FALSE;
        }
        $landing_zone = get_share_landing_zone($share);
        if (!$landing_zone) {
            return FALSE;
        }
        $real_fullpath = "$landing_zone/$full_path";
        $result = gh_is_file_locked($real_fullpath);
        if ($result !== FALSE) {
            $db_spool->lockFile($idx, $result);
            return $result;
        }
        $query = "SELECT * FROM tasks WHERE complete = 'no' AND action = 'write' AND share = :share AND full_path = :full_path LIMIT 1";
        $params = array('share' => $share, 'full_path' => $full_path);
        $row = DB::getFirst($query, $params);
        if ($row === FALSE) {
            return FALSE;
        }
        if (!gh_file_exists($real_fullpath)) {
            $query = "UPDATE tasks SET complete = 'yes' WHERE complete = 'no' AND action = 'write' AND share = :share AND full_path = :full_path";
            DB::execute($query, $params);
            return FALSE;
        }
        $result = 'samba-vfs-writer';
        $db_spool->lockFile($idx, $result);
        return $result;
    }
    public function execute_next_task() {
        if (!empty($this->next_tasks)) {
            $task = array_shift($this->next_tasks);
        } else {
            $this->next_tasks = $this->fetch_next_tasks(TRUE, TRUE);
            if (!empty($this->next_tasks)) {
                $task = array_shift($this->next_tasks);
            } else {
                Log::setAction(ACTION_SLEEP);
                DB::repairTables();
                Md5Task::check_md5_workers();
                Log::cleanStatusTable();
                foreach (array('fsck_checksums.log', 'fsck_files.log') as $log_file) {
                    $log = new FSCKLogFile($log_file);
                    $log->emailAsRequired();
                }
                $log = "Nothing to do... Sleeping.";
                Log::debug($log);
                if (!DaemonRunner::$was_idle) {
                    LogHook::trigger(LogHook::EVENT_TYPE_IDLE, Log::EVENT_CODE_IDLE, $log);
                    DaemonRunner::$was_idle = TRUE;
                }
                $log_level = Log::getLevel();
                sleep($log_level == Log::DEBUG ? 10 : ($log_level == Log::TEST || $log_level == Log::PERF ? 1 : 600));
                $this->locked_files = array();
                $this->locked_shares = array();
                return;
            }
        }
        $task = AbstractTask::instantiate($task);
        $this->current_task = $task;
        if (array_contains($this->sleep_before_task, $task->id)) {
            Log::setAction(ACTION_SLEEP);
            $log = "Only locked files operations pending... Sleeping.";
            Log::debug($log);
            if (!DaemonRunner::$was_idle) {
                LogHook::trigger(LogHook::EVENT_TYPE_IDLE, Log::EVENT_CODE_IDLE, $log);
                DaemonRunner::$was_idle = TRUE;
            }
            $log_level = Log::getLevel();
            sleep($log_level == Log::DEBUG ? 10 : ($log_level == Log::TEST ? 1 : 600));
            $this->locked_files = array();
            $this->sleep_before_task = array();
        }
        Log::setAction($task->action);
        $log = "Now working on task ID $task->id: $task->action " . clean_dir("$task->share/$task->full_path") . ($task->action == 'rename' ? " -> $task->share/$task->additional_info" : '');
        Log::info($log);
        if ($task->complete == 'written') {
            if ($task->should_ignore_file()) {
                static::archive_task($task->id);
                return;
            }
            $filename = get_share_landing_zone($task->share) . '/' . $task->full_path;
            $filesize = gh_filesize($filename);
            if (empty($this->written_files[clean_dir("$task->share/$task->full_path")])) {
                $this->written_files[clean_dir("$task->share/$task->full_path")] = (object) array('since' => time(), 'filesize' => $filesize);
            } else {
                $infos = $this->written_files[clean_dir("$task->share/$task->full_path")];
                if ($infos->filesize == $filesize) {
                    if (time() - $infos->since > 10*60) {
                        Log::debug("  File is still being written to (" . bytes_to_human($filesize, FALSE) . "). But it's been at least 10 minutes since the file size changed. We can probably assume we should work on this file now. Let do this!");
                        unset($this->written_files[clean_dir("$task->share/$task->full_path")]);
                    }
                } else {
                    $this->written_files[clean_dir("$task->share/$task->full_path")] = (object) array('since' => time(), 'filesize' => $filesize);
                }
            }
            if (!empty($this->written_files[clean_dir("$task->share/$task->full_path")])) {
                Log::debug("  File is still being written to (" . bytes_to_human($filesize, FALSE) . "). Postponing.");
                $this->lockFile(clean_dir("$task->share/$task->full_path"), 'samba-bytes-writer');
                $this->locked_shares[$task->share] = TRUE;
                return;
            }
        }
        if ($task->shouldBeFrozen()) {
            Log::debug("  This directory is frozen. Will postpone this task until it is thawed.");
            if ($task->action == 'write') {
                $q = "SELECT id FROM tasks WHERE action = 'write' AND share = :share AND full_path = :full_path AND complete IN ('yes', 'thawed', 'idle') AND id > :task_id";
                $duplicate_tasks_to_delete = DB::getAllValues($q, ['share' => $task->share, 'full_path' => $task->full_path, 'task_id' => $task->id]);
                if (!empty($duplicate_tasks_to_delete)) {
                    Log::debug("  Deleting " . count($duplicate_tasks_to_delete) . " future 'write' tasks that are duplicate of this one.");
                    DBSpool::getInstance()->delete_tasks($duplicate_tasks_to_delete);
                }
            }
            $this->postpone_task($task->id, 'frozen');
            static::archive_task($task->id);
            return;
        }
        if (!empty($this->locked_shares) && array_contains(array_keys($this->locked_shares), $task->share)) {
            Log::info("  Share is locked because another file operation is waiting for a file handle to be released. Skipping.");
            return;
        }
        if (DaemonRunner::$was_idle) {
            LogHook::trigger(LogHook::EVENT_TYPE_NOT_IDLE, Log::EVENT_CODE_IDLE_NOT, $log);
            DaemonRunner::$was_idle = FALSE;
        }
        $result = $task->execute();
        if (!$result) {
            return;
        }
        if ($task->action != 'write' && $task->action != 'rename') {
            $this->sleep_before_task = array();
        }
        static::archive_task($task->id);
    }
    public function insert($action, $share, $full_path, $additional_info, $fd) {
        $query = "INSERT INTO tasks SET action = :action, share = :share, full_path = :full_path, additional_info = :additional_info, complete = :complete";
        $full_path = isset($full_path) ? clean_dir($full_path) : NULL;
        $additional_info = !empty($additional_info) ? clean_dir($additional_info) : (!empty($fd) ? $fd : NULL);
        $params = array(
            'action' => $action,
            'share' => $share,
            'full_path' => $full_path,
            'additional_info' => $additional_info,
            'complete' => ( $action == 'write' ? 'no' : 'yes' ),
        );
        try {
            $id = DB::insert($query, $params);
        } catch (Exception $ex) {
            if ($ex->getCode() == 1366) {
                Log::warn("File '$full_path' contains non UTF-8 character. Skipping.", Log::EVENT_CODE_FILE_INVALID);
                return FALSE;
            }
        }
        return $id;
    }
    public function close_task($act, $share, $fd, $fullpath, &$tasks) {
        if (!empty($fullpath)) {
            $prop = 'full_path';
            $prop_value = $fullpath;
        } else {
            $prop = 'additional_info';
            $prop_value = $fd;
        }
        if ($act === 'fwrite') {
            if (!empty($fullpath)) {
                $q = "SELECT * FROM tasks WHERE action = 'write' AND complete IN ('written', 'no') AND share = :share AND $prop = :$prop";
                try {
                    $opened_task = DB::getFirst($q, array('share' => $share, $prop => $prop_value));
                } catch (Exception $ex) {
                    if ($ex->getCode() == 1267) {
                        Log::warn("File '$prop_value' contains non UTF-8 character. Skipping.", Log::EVENT_CODE_FILE_INVALID);
                        return;
                    }
                }
                if (empty($opened_task)) {
                    $id = $this->insert('write', $share, $fullpath, NULL, $fd);
                    $q = "UPDATE tasks SET complete = 'yes' WHERE id = :id";
                    DB::execute($q, array('id' => $id));
                    $q = "SELECT * FROM tasks WHERE id = :id";
                    $opened_task = DB::getFirst($q, array('id' => $id));
                }
            }
            if (empty($fullpath) || $opened_task->complete == 'no') {
                $query = "UPDATE tasks SET complete = 'written' WHERE action = 'write' AND complete = 'no' AND share = :share AND $prop = :$prop";
                DB::execute($query, array('share' => $share, $prop => $prop_value));
            }
        }
        if ($act === 'close') {
            if ($this->use_old_vfs) {
                $query = "UPDATE tasks SET additional_info = NULL, complete = 'yes' WHERE complete = 'no' AND share = :share AND $prop = :$prop";
                DB::execute($query, array('share' => $share, $prop => $prop_value));
            } else {
                $last_id = DB::getFirstValue("SELECT MAX(id) FROM tasks");
                if ($last_id && $fullpath != '.') {
                    $task = (object) array(
                        'share' => $share,
                        'fd' => $fd,
                        'full_path' => $fullpath,
                        'last_id' => $last_id,
                    );
                    $tasks[md5("$share/$fullpath<$last_id")] = $task;
                }
            }
        }
    }
    public function close_all_tasks($tasks) {
        $q = "SELECT COUNT(*) FROM tasks WHERE complete = 'no'";
        $has_incomplete_tasks = (int) DB::getFirstValue($q);
        $q = "SELECT COUNT(*) FROM tasks WHERE complete = 'written'";
        $has_written_tasks = (int) DB::getFirstValue($q);
        if (!$has_incomplete_tasks && !$has_written_tasks) {
            Log::perf("  There are no complete=written or complete=no write tasks. No point looking into each individual close task...");
            return;
        }
        foreach ($tasks as $task) {
            $share = $task->share;
            $fd = $task->fd;
            $full_path = $task->full_path;
            $last_id = $task->last_id;
            $params = array('share' => $share, 'last_id' => $last_id);
            if (!empty($full_path)) {
                $prop = 'full_path';
                $params[$prop] = $full_path;
            } else {
                $prop = 'additional_info';
                $params[$prop] = $fd;
            }
            if ($has_written_tasks) {
                Log::perf("  Closing (complete=written) write tasks for $share/{$params[$prop]} (WHERE id <= $last_id)");
                $query = "UPDATE tasks SET additional_info = NULL, complete = 'yes' WHERE complete = 'written' AND share = :share AND $prop = :$prop AND id <= :last_id";
                DB::execute($query, $params);
            }
            if ($has_incomplete_tasks <= 0) {
                continue;
            }
            Log::perf("  Closing (complete=no) write tasks for $share/{$params[$prop]} (WHERE id <= $last_id)");
            $query = "SELECT id, full_path FROM tasks WHERE complete = 'no' AND share = :share AND $prop = :$prop AND id <= :last_id";
            $rows = DB::getAll($query, $params);
            foreach ($rows as $row) {
                $file_fullpath = get_share_landing_zone($share) . '/' . $row->full_path;
                $size = gh_filesize($file_fullpath);
                if ($size == 0) {
                    $query = "UPDATE tasks SET additional_info = NULL, complete = 'yes' WHERE id = :task_id";
                    DB::execute($query, array('task_id' => $row->id));
                } else {
                    Log::debug("File pointer to $share/$row->full_path was closed without being written to. Ignoring.");
                }
                $has_incomplete_tasks--;
            }
            if (empty($rows)) {
                Log::perf("    Found no writes.");
                continue;
            }
            $query = "DELETE FROM tasks WHERE complete = 'no' AND share = :share AND $prop = :$prop AND id <= :last_id";
            DB::execute($query, $params);
        }
    }
    public static function archive_task($task_id) {
        $query = "INSERT INTO tasks_completed SELECT * FROM tasks WHERE id = :task_id";
        $worked = DB::insert($query, array('task_id' => $task_id));
        if (!$worked) {
            DB::connect();
            DB::insert($query, array('task_id' => $task_id));
        }
        $query = "DELETE FROM tasks WHERE id = :task_id";
        DB::execute($query, array('task_id' => $task_id));
    }
    public function postpone_task($task_id, $complete='yes') {
        $query = "INSERT INTO tasks (action, share, full_path, additional_info, complete) SELECT action, share, full_path, additional_info, :complete FROM tasks WHERE id = :task_id";
        $params = array(
            'complete' => $complete,
            'task_id' => $task_id
        );
        DB::insert($query, $params);
        $this->sleep_before_task[] = DB::lastInsertedId();
    }
    public function delete_tasks($task_ids) {
        if (empty($task_ids)) {
            return;
        }
        if (is_string($task_ids)) {
            $task_ids = explode(',', $task_ids);
        }
        if (is_array($task_ids)) {
            foreach ($this->next_tasks as $k => $task) {
                if (array_contains($task_ids, $task->id)) {
                    unset($this->next_tasks[$k]);
                }
            }
            $this->next_tasks = array_values($this->next_tasks);
            $task_ids = implode(',', $task_ids);
        }
        DB::execute("DELETE FROM tasks WHERE id IN ($task_ids)");
    }
    public function find_next_rename_task($share, $full_path, $task_id) {
        $full_paths = [$full_path];
        $parent_full_path = $full_path;
        list($parent_full_path, ) = explode_full_path($parent_full_path);
        while (strlen($parent_full_path) > 1) {
            $full_paths[] = $parent_full_path;
            list($parent_full_path, ) = explode_full_path($parent_full_path);
        }
        $params = ['share' => $share, 'task_id' => $task_id];
        $param_names = [];
        foreach ($full_paths as $i => $full_path) {
            $param_name = sprintf("fp_%03d", $i);
            $param_names[] = ":$param_name";
            $params[$param_name] = $full_path;
        }
        $query = "SELECT * FROM tasks WHERE complete = 'yes' AND share = :share AND action = 'rename' AND full_path IN (" . implode(", ", $param_names) . ") AND id > :task_id ORDER BY id LIMIT 1";
        return DB::getFirst($query, $params);
    }
    public static function get_num_tasks($action = NULL) {
        $query = "SELECT COUNT(*) FROM tasks";
        $params = [];
        if (!empty($action)) {
            $query .= " WHERE action = :action";
            $params['action'] = $action;
        }
        return (int) DB::getFirstValue($query, $params);
    }
}

abstract class Hook
{
    protected $event_type;
    protected $script;
    protected static $hooks = array();
    public function __construct($event_type, $script) {
        $this->event_type = $event_type;
        $this->script = $script;
    }
    public static function add($event_type, $script) {
        if (array_contains(FileHook::getEventTypes(), $event_type)) {
            $hook = new FileHook($event_type, $script);
        } elseif (array_contains(LogHook::getEventTypes(), $event_type)) {
            $hook = new LogHook($event_type, $script);
        } elseif ($event_type == 'email') {
            $hook = new EmailHook($event_type, $script);
        } else {
            Log::warn("Unknown hook event type '$event_type'; ignoring.", Log::EVENT_CODE_HOOK_NOT_EXECUTABLE);
            return;
        }
        static::$hooks[$event_type][] = $hook;
    }
    public static function hasHookForEvent($event_type) {
        return !empty(static::$hooks[$event_type]);
    }
    protected static function _trigger($event_type, $context) {
        if (!isset(static::$hooks[$event_type])) {
            return;
        }
        $hooks = static::$hooks[$event_type];
        if (!is_array($hooks)) {
            return;
        }
        foreach ($hooks as $hook) {
            Log::debug("Calling external hook $hook->script for event $hook->event_type ...");
            exec(escapeshellarg($hook->script) . " " . implode(' ', $hook->getArgs($context)) . " 2>&1", $output, $result_code);
            if (!empty($output)) {
                foreach($output as $line) {
                    Log::debug("  $line");
                }
            }
            if ($result_code === 0) {
                Log::debug("External hook exited with status code $result_code.");
            } else {
                if ($hook->event_type == LogHook::EVENT_TYPE_WARNING) {
                } else {
                    Log::warn("External hook $hook->script exited with status code $result_code.", LogHook::EVENT_CODE_HOOK_NON_ZERO_EXIT_CODE_IN_WARN);
                }
            }
        }
    }
    abstract protected function getArgs($context);
}
interface HookContext {}
class FileHookContext implements HookContext
{
    public $share;
    public $path_on_share;
    public $from_path_on_share;
    public function __construct($share, $path_on_share, $from_path_on_share = NULL) {
        $this->share = $share;
        $this->path_on_share = $path_on_share;
        $this->from_path_on_share = $from_path_on_share;
    }
}
class FileHook extends Hook
{
    const EVENT_TYPE_CREATE = 'create';
    const EVENT_TYPE_EDIT   = 'edit';
    const EVENT_TYPE_RENAME = 'rename';
    const EVENT_TYPE_DELETE = 'delete';
    const EVENT_TYPE_MKDIR  = 'mkdir';
    const EVENT_TYPE_RMDIR  = 'rmdir';
    public static function trigger($event_type, $share, $path_on_share, $from_path_on_share = NULL) {
        Hook::_trigger($event_type, new FileHookContext($share, $path_on_share, $from_path_on_share));
    }
    protected function getArgs($context) {
        $args = array(
            escapeshellarg($this->event_type),
            escapeshellarg($context->share),
            escapeshellarg($context->path_on_share)
        );
        if (!empty($context->from_path_on_share)) {
            $args[] = escapeshellarg($context->from_path_on_share);
        }
        return $args;
    }
    public static function getEventTypes() {
        return array(
            static::EVENT_TYPE_CREATE,
            static::EVENT_TYPE_EDIT,
            static::EVENT_TYPE_RENAME,
            static::EVENT_TYPE_DELETE,
            static::EVENT_TYPE_MKDIR,
            static::EVENT_TYPE_RMDIR
        );
    }
}
class LogHookContext implements HookContext
{
    public $event_code;
    public $log;
    public function __construct($event_code, $log) {
        $this->event_code = $event_code;
        $this->log = $log;
    }
}
class LogHook extends Hook
{
    const EVENT_TYPE_WARNING  = 'warning';
    const EVENT_TYPE_ERROR    = 'error';
    const EVENT_TYPE_CRITICAL = 'critical';
    const EVENT_TYPE_IDLE     = 'idle';
    const EVENT_TYPE_NOT_IDLE = 'not_idle';
    const EVENT_TYPE_FSCK     = 'fsck';
    const EVENT_CODE_HOOK_NON_ZERO_EXIT_CODE_IN_WARN = 1; // Used to prevent infinite loop, when logging a WARNING from a LogHook!
    public static function trigger($event_type, $event_code, $log) {
        Hook::_trigger($event_type, new LogHookContext($event_code, $log));
    }
    protected function getArgs($context) {
        return array(
            escapeshellarg($this->event_type),
            escapeshellarg($context->event_code),
            escapeshellarg($context->log)
        );
    }
    public static function getEventTypes() {
        return array(
            static::EVENT_TYPE_WARNING,
            static::EVENT_TYPE_ERROR,
            static::EVENT_TYPE_CRITICAL,
            static::EVENT_TYPE_IDLE,
            static::EVENT_TYPE_NOT_IDLE,
            static::EVENT_TYPE_FSCK
        );
    }
}
class EmailHookContext implements HookContext
{
    public $subject;
    public $body;
    public function __construct($subject, $body) {
        $this->subject = $subject;
        $this->body = $body;
    }
}
class EmailHook extends Hook
{
    public static function trigger($subject, $body) {
        Hook::_trigger('email', new EmailHookContext($subject, $body));
    }
    protected function getArgs($context) {
        return array(
            escapeshellarg($this->event_type),
            escapeshellarg($context->subject),
            escapeshellarg($context->body)
        );
    }
}

define('ACTION_INITIALIZE', 'initialize');
define('ACTION_UNKNOWN', 'unknown');
define('ACTION_DAEMON', 'daemon');
define('ACTION_PAUSE', 'pause');
define('ACTION_RESUME', 'resume');
define('ACTION_FSCK', 'fsck');
define('ACTION_CANCEL_FSCK', 'cancel-fsck');
define('ACTION_BALANCE', 'balance');
define('ACTION_CANCEL_BALANCE', 'cancel-balance');
define('ACTION_STATS', 'stats');
define('ACTION_STATUS', 'status');
define('ACTION_LOGS', 'logs');
define('ACTION_EMPTY_TRASH', 'empty-trash');
define('ACTION_VIEW_QUEUE', 'view-queue');
define('ACTION_IOSTAT', 'iostat');
define('ACTION_GETUID', 'getuid');
define('ACTION_MD5_WORKER', 'md5-worker');
define('ACTION_FIX_SYMLINKS', 'fix-symlinks');
define('ACTION_REPLACE', 'replaced');
define('ACTION_WAIT_FOR', 'wait-for');
define('ACTION_REMOVE', 'remove');
define('ACTION_THAW', 'thaw');
define('ACTION_DEBUG', 'debug');
define('ACTION_DELETE_METADATA', 'delete-metadata');
define('ACTION_REMOVE_SHARE', 'remove-share');
define('ACTION_CHECK_POOL', 'check_pool');
define('ACTION_SLEEP', 'sleep');
define('ACTION_READ_SAMBA_POOL', 'read_smb_spool');
define('ACTION_FSCK_FILE', 'fsck_file');
define('ACTION_MOVE', 'move');
define('ACTION_CP', 'cp');
final class Log {
    const PERF     = 9;
    const TEST     = 8;
    const DEBUG    = 7;
    const INFO     = 6;
    const WARN     = 4;
    const ERROR    = 3;
    const CRITICAL = 2;
    const EVENT_CODE_ALL_DRIVES_FULL = 'all_drives_full';
    const EVENT_CODE_CONFIG_DEPRECATED_OPTION = 'config_deprecated_option';
    const EVENT_CODE_CONFIG_TESTPARM_FAILED = 'config_testparm_failed';
    const EVENT_CODE_CONFIG_FILE_PARSING_FAILED = 'config_file_parsing_failed';
    const EVENT_CODE_CONFIG_HOOK_SCRIPT_NOT_EXECUTABLE = 'config_hook_script_not_executable';
    const EVENT_CODE_CONFIG_INCLUDE_INSECURE_PERMISSIONS = 'config_include_insecure_permissions';
    const EVENT_CODE_CONFIG_INVALID_VALUE = 'config_invalid_value';
    const EVENT_CODE_CONFIG_LZ_INSIDE_STORAGE_POOL = 'config_lz_inside_storage_pool';
    const EVENT_CODE_CONFIG_NO_STORAGE_POOL = 'config_no_storage_pool';
    const EVENT_CODE_CONFIG_SHARE_MISSING_FROM_SMB_CONF = 'config_share_missing_from_smb_conf';
    const EVENT_CODE_CONFIG_STORAGE_POOL_DRIVES_SAME_PARTITION = 'config_storage_pool_drives_same_partition';
    const EVENT_CODE_CONFIG_STORAGE_POOL_DRIVE_NOT_IN_DRIVE_SELECTION_ALGO = 'config_storage_pool_drive_not_in_drive_selection_algo';
    const EVENT_CODE_CONFIG_STORAGE_POOL_INSIDE_LZ = 'config_storage_pool_inside_lz';
    const EVENT_CODE_CONFIG_UNPARSEABLE_LINE = 'config_unparseable_line';
    const EVENT_CODE_DB_CONNECT_FAILED = 'db_connect_failed';
    const EVENT_CODE_DB_MIGRATION_FAILED = 'db_migration_failed';
    const EVENT_CODE_DB_TABLE_CRASHED = 'db_table_crashed';
    const EVENT_CODE_DB_TZ_CHANGE_FAILED = 'db_tz_change_failed';
    const EVENT_CODE_FILE_COPY_FAILED = 'file_copy_failed';
    const EVENT_CODE_FILE_INVALID = 'file_invalid_character';
    const EVENT_CODE_FSCK_REPORT = 'fsck_report';
    const EVENT_CODE_FSCK_EMPTY_FILE_COPY_FOUND = 'fsck_empty_file_copy_found';
    const EVENT_CODE_FSCK_MD5_LOG_FAILURE = 'fsck_md5_log_failure';
    const EVENT_CODE_FSCK_MD5_MISMATCH = 'fsck_md5_mismatch';
    const EVENT_CODE_FSCK_METAFILE_ROOT_PATH_NOT_FOUND = 'fsck_metafile_root_path_not_found';
    const EVENT_CODE_FSCK_NO_FILE_COPIES = 'fsck_no_file_copies';
    const EVENT_CODE_FSCK_SIZE_MISMATCH_FILE_COPY_FOUND = 'fsck_size_mismatch_file_copy_found';
    const EVENT_CODE_FSCK_SYMLINK_FOUND_IN_STORAGE_POOL = 'fsck_symlink_found_in_storage_pool';
    const EVENT_CODE_FSCK_UNKNOWN_FOLDER = 'fsck_unknown_folder';
    const EVENT_CODE_FSCK_UNKNOWN_SHARE = 'fsck_unknown_share';
    const EVENT_CODE_HOOK_NON_ZERO_EXIT_CODE = 'hook_non_zero_exit_code';
    const EVENT_CODE_HOOK_NOT_EXECUTABLE = 'hook_not_executable';
    const EVENT_CODE_IDLE = "idle";
    const EVENT_CODE_IDLE_NOT = "busy";
    const EVENT_CODE_LIST_DIR_FAILED = 'list_dir_failed';
    const EVENT_CODE_MEMORY_LIMIT_REACHED = 'memory_limit_reached';
    const EVENT_CODE_METADATA_POINTS_TO_GONE_DRIVE = 'metadata_points_to_gone_drive';
    const EVENT_CODE_MKDIR_CHGRP_FAILED = 'mkdir_chgrp_failed';
    const EVENT_CODE_MKDIR_CHMOD_FAILED = 'mkdir_chmod_failed';
    const EVENT_CODE_MKDIR_CHOWN_FAILED = 'mkdir_chown_failed';
    const EVENT_CODE_MKDIR_FAILED = 'mkdir_failed';
    const EVENT_CODE_NO_METADATA_SAVED = 'no_metadata_saved';
    const EVENT_CODE_PHP_CRITICAL = 'php_critical';
    const EVENT_CODE_PHP_ERROR = 'php_error';
    const EVENT_CODE_PHP_WARNING = 'php_warning';
    const EVENT_CODE_RENAME_FILE_COPY_FAILED = 'rename_file_copy_failed';
    const EVENT_CODE_SAMBA_RESTART_FAILED = 'samba_restart_failed';
    const EVENT_CODE_SETTINGS_READ_ERROR = 'settings_read_error';
    const EVENT_CODE_SHARE_MISSING_FROM_GREYHOLE_CONF = 'share_missing_from_greyhole_conf';
    const EVENT_CODE_SPOOL_MOUNT_FAILED = 'spool_mount_failed';
    const EVENT_CODE_STORAGE_POOL_DRIVE_DF_FAILED = 'storage_pool_drive_df_failed';
    const EVENT_CODE_STORAGE_POOL_DRIVE_UUID_CHANGED = 'storage_pool_drive_uuid_changed';
    const EVENT_CODE_STORAGE_POOL_FOLDER_NOT_FOUND = 'storage_pool_folder_not_found';
    const EVENT_CODE_TASK_FOR_UNKNOWN_SHARE = 'task_for_unknown_share';
    const EVENT_CODE_TRASH_NOT_FOUND = 'trash_not_found';
    const EVENT_CODE_TRASH_SYMLINK_FAILED = 'trash_symlink_failed';
    const EVENT_CODE_UNEXPECTED_VAR = 'unexpected_var';
    const EVENT_CODE_VFS_MODULE_WRONG = 'vfs_module_wrong';
    const EVENT_CODE_ZFS_UNKNOWN_DEVICE = 'zfs_unknown_device';
    private static $log_level_names = array(
        9 => 'PERF',
        8 => 'TEST',
        7 => 'DEBUG',
        6 => 'INFO',
        4 => 'WARN',
        3 => 'ERROR',
        2 => 'CRITICAL',
    );
    private static $action = ACTION_INITIALIZE;
    private static $old_action;
    private static $level = Log::INFO; // Default, until we are able to read the config file
    public static function setLevel($level) {
        self::$level = $level;
    }
    public static function getLevel() {
        return self::$level;
    }
    public static function setAction($action) {
        self::$old_action = self::$action;
        self::$action = str_replace(':', '', $action);
    }
    public static function actionIs($action) {
        return self::$action == $action;
    }
    public static function restorePreviousAction() {
        self::$action = self::$old_action;
    }
    public static function perf($text, $event_code = NULL) {
        self::_log(self::PERF, $text, $event_code);
    }
    public static function debug($text, $event_code = NULL) {
        self::_log(self::DEBUG, $text, $event_code);
    }
    public static function info($text, $event_code = NULL) {
        self::_log(self::INFO, $text, $event_code);
    }
    public static function warn($text, $event_code) {
        self::_log(self::WARN, $text, $event_code);
    }
    public static function error($text, $event_code) {
        self::_log(self::ERROR, $text, $event_code);
    }
    public static function critical($text, $event_code) {
        self::_log(self::CRITICAL, $text, $event_code);
    }
    private static $last_log;
    private static function _log($local_log_level, $text, $event_code) {
        if (self::$action == 'test-config' || self::$action == ACTION_CP) {
            $greyhole_log_file = NULL;
            $use_syslog = FALSE;
            if (self::$action == ACTION_CP && $local_log_level > self::$level) {
                return;
            }
        } elseif (self::$action == ACTION_INITIALIZE && !DaemonRunner::isCurrentProcessDaemon()) {
            if ($local_log_level === self::CRITICAL) {
                if (defined('IS_WEB_APP')) {
                    throw new Exception($text);
                }
                exit(1);
            }
            return;
        } else {
            if (DaemonRunner::isCurrentProcessDaemon() && self::$action != ACTION_READ_SAMBA_POOL) {
                try {
                    if ($text != static::$last_log) { // Don't log duplicates (Sleeping... for example)
                        $q = "INSERT INTO status SET log = :log, action = :action";
                        DB::insert($q, array('log' => $text, 'action' => empty(self::$action) ? ACTION_UNKNOWN : self::$action));
                    }
                    static::$last_log = $text;
                } catch (\Exception $ex) {
                    $text .= " [ERROR logging status in database: " . $ex->getMessage() . "]";
                    $local_log_level = self::ERROR;
                }
            }
            if ($local_log_level > self::$level) {
                return;
            }
            $greyhole_log_file = Config::get(CONFIG_GREYHOLE_LOG_FILE);
            $use_syslog = strtolower($greyhole_log_file) == 'syslog';
        }
        $date = date("M d H:i:s");
        if (self::$level >= self::PERF) {
            $utimestamp = microtime(true);
            $timestamp = floor($utimestamp);
            $date .= '.' . round(($utimestamp - $timestamp) * 1000000);
        }
        $log_level_string = $use_syslog ? $local_log_level : self::$log_level_names[$local_log_level];
        $log_text = sprintf("%s%s%s\n",
            "$date $log_level_string " . self::$action . ": ",
            $text,
            Config::get(CONFIG_LOG_MEMORY_USAGE) ? " [" . memory_get_usage() . "]" : ''
        );
        if ($use_syslog) {
            $worked = syslog($local_log_level, $log_text);
        } else if (!empty($greyhole_log_file)) {
            $worked = @error_log($log_text, 3, $greyhole_log_file);
            $greyhole_error_log_file = Config::get(CONFIG_GREYHOLE_ERROR_LOG_FILE);
            if ($local_log_level <= self::WARN && !empty($greyhole_error_log_file)) {
                $worked &= @error_log($log_text, 3, $greyhole_error_log_file);
            }
        } else {
            $worked = FALSE;
        }
        if (!$worked || $local_log_level === self::CRITICAL) {
            error_log(trim($log_text));
        }
        if ($local_log_level == self::WARN) {
            if ($event_code != LogHook::EVENT_CODE_HOOK_NON_ZERO_EXIT_CODE_IN_WARN) {
                LogHook::trigger(LogHook::EVENT_TYPE_WARNING, $event_code, $log_text);
            }
        } elseif ($local_log_level == self::ERROR) {
            LogHook::trigger(LogHook::EVENT_TYPE_ERROR, $event_code, $log_text);
        } elseif ($local_log_level == self::CRITICAL) {
            LogHook::trigger(LogHook::EVENT_TYPE_CRITICAL, $event_code, $log_text);
        }
        if ($local_log_level === self::CRITICAL) {
            if (defined('IS_WEB_APP')) {
                throw new Exception($text);
            }
            exit(1);
        }
    }
    public static function cleanStatusTable() {
        $q = "SELECT MAX(id) FROM status";
        $max_id = DB::getFirstValue($q);
        $q = "DELETE FROM status WHERE id < :id";
        DB::execute($q, array('id' => max(0, $max_id - 100)));
    }
}

final class Metastores {
    const METASTORE_DIR = '.gh_metastore';
    const METASTORE_BACKUP_DIR = '.gh_metastore_backup';
    private static $metastores = [];
    public static function get_metastores($use_cache=TRUE) {
        if (!$use_cache) {
            static::$metastores = [];
        }
        if (empty(static::$metastores)) {
            foreach (Config::storagePoolDrives() as $sp_drive) {
                if (StoragePool::is_pool_drive($sp_drive)) {
                    static::$metastores[] = $sp_drive . '/' . static::METASTORE_DIR;
                }
            }
            foreach (Config::get(CONFIG_METASTORE_BACKUPS) as $metastore_backup_drive) {
                $sp_drive = str_replace('/' . static::METASTORE_BACKUP_DIR, '', $metastore_backup_drive);
                if (StoragePool::is_pool_drive($sp_drive)) {
                    static::$metastores[] = $metastore_backup_drive;
                }
            }
        }
        return static::$metastores;
    }
    public static function choose_metastores_backups($try_restore=TRUE) {
        $num_metastore_backups_needed = 2;
        if (count(Config::storagePoolDrives()) < 2) {
            Config::set(CONFIG_METASTORE_BACKUPS, array());
            return;
        }
        Log::debug("Loading metadata store backup directories...");
        $metastore_backup_drives = Config::get(CONFIG_METASTORE_BACKUPS);
        if (empty($metastore_backup_drives)) {
            $metastore_backup_drives = Settings::get('metastore_backup_directory', TRUE);
            if ($metastore_backup_drives) {
                Log::debug("  Found " . count($metastore_backup_drives) . " directories in the settings table.");
            } elseif ($try_restore) {
                if (Settings::restore()) {
                    static::choose_metastores_backups(FALSE);
                    return;
                }
            }
        }
        if (empty($metastore_backup_drives)) {
            $metastore_backup_drives = array();
        } else {
            foreach ($metastore_backup_drives as $key => $metastore_backup_drive) {
                if (!StoragePool::is_pool_drive(str_replace('/' . static::METASTORE_BACKUP_DIR, '', $metastore_backup_drive))) {
                    Log::debug("Removing $metastore_backup_drive from available 'metastore_backup_directories' - this directory isn't a Greyhole storage pool drive (anymore?)");
                    unset($metastore_backup_drives[$key]);
                } elseif (!is_dir($metastore_backup_drive)) {
                    mkdir($metastore_backup_drive);
                }
            }
        }
        if (empty($metastore_backup_drives) || count($metastore_backup_drives) < $num_metastore_backups_needed) {
            Log::debug("  Missing some drives. Need $num_metastore_backups_needed, currently have " . count($metastore_backup_drives) . ". Will select more...");
            $metastore_backup_drives_hash = array();
            if (count($metastore_backup_drives) > 0) {
                $metastore_backup_drives_hash[array_shift($metastore_backup_drives)] = TRUE;
            }
            while (count($metastore_backup_drives_hash) < $num_metastore_backups_needed) {
                $metastore_backup_drive = ConfigHelper::randomStoragePoolDrive() . '/' . static::METASTORE_BACKUP_DIR;
                $metastore_backup_drives_hash[$metastore_backup_drive] = TRUE;
                if (!is_dir($metastore_backup_drive)) {
                    mkdir($metastore_backup_drive);
                }
                Log::debug("    Randomly picked $metastore_backup_drive");
            }
            $metastore_backup_drives = array_keys($metastore_backup_drives_hash);
            Settings::set('metastore_backup_directory', $metastore_backup_drives);
        }
        Config::set(CONFIG_METASTORE_BACKUPS, $metastore_backup_drives);
    }
    public static function dir_exists_in_metastores($share, $full_path) {
        foreach (static::get_metastores() as $metastore) {
            if (is_dir("$metastore/$share/$full_path")) {
                return TRUE;
            }
        }
        return FALSE;
    }
    public static function get_metastore_from_path($path) {
        $metastore_path = FALSE;
        foreach (static::get_metastores() as $metastore) {
            if (string_starts_with($path, $metastore)) {
                $metastore_path = $metastore;
                break;
            }
        }
        return $metastore_path;
    }
    public static function get_metastores_from_storage_volume($storage_volume) {
        $volume_metastores = array();
        foreach (static::get_metastores() as $metastore) {
            if (StoragePool::getDriveFromPath($metastore) == $storage_volume) {
                $volume_metastores[] = $metastore;
            }
        }
        return $volume_metastores;
    }
    public static function get_metafile_data_filenames($share, $path, $filename, $first_only=FALSE) {
        $filenames = array();
        if ($first_only) {
            $share_file = get_share_landing_zone($share) . "/$path/$filename";
            if (is_link($share_file)) {
                $target = readlink($share_file);
                $first_metastore = str_replace(clean_dir("/$share/$path/$filename"), "", $target);
                $f = clean_dir("$first_metastore/" . static::METASTORE_DIR . "/$share/$path/$filename");
                if (is_file($f)) {
                    $filenames[] = $f;
                    return $filenames;
                }
            }
        }
        foreach (static::get_metastores() as $metastore) {
            $f = clean_dir("$metastore/$share/$path/$filename");
            if (is_file($f)) {
                $filenames[] = $f;
                if ($first_only) {
                    return $filenames;
                }
            }
        }
        return $filenames;
    }
    public static function get_metafile_data_filename($share, $path, $filename) {
        $filenames = static::get_metafile_data_filenames($share, $path, $filename, TRUE);
        return first($filenames, FALSE);
    }
    public static function get_metafiles($share, $path, $filename=NULL, $load_nok_metafiles=FALSE, $quiet=FALSE, $check_symlink=TRUE) {
        if ($filename === NULL) {
            return new metafile_iterator($share, $path, $load_nok_metafiles, $quiet, $check_symlink);
        } else {
            return array(static::get_metafiles_for_file($share, $path, $filename, $load_nok_metafiles, $quiet, $check_symlink));
        }
    }
    public static function get_metafiles_for_file($share, $path, $filename=NULL, $load_nok_metafiles=FALSE, $quiet=FALSE, $check_symlink=TRUE) {
        if (!$quiet) {
            Log::debug("Loading metafiles for " . clean_dir($share . (!empty($path) ? "/$path" : "") . "/$filename") . ' ...');
        }
        $metafiles_data_file = static::get_metafile_data_filename($share, $path, $filename);
        clearstatcache();
        $metafiles = array();
        if (file_exists($metafiles_data_file)) {
            $t = file_get_contents($metafiles_data_file);
            $metafiles = unserialize($t);
        }
        if (!is_array($metafiles)) {
            $metafiles = array();
        }
        if ($check_symlink) {
            $share_file = get_share_landing_zone($share) . "/$path/$filename";
            if (is_link($share_file)) {
                $share_file_link_to = readlink($share_file);
                if ($share_file_link_to !== FALSE) {
                    foreach ($metafiles as $key => $metafile) {
                        if ($metafile->state == Metafile::STATE_OK) {
                            if (@$metafile->is_linked && $metafile->path != $share_file_link_to) {
                                if (!$quiet) {
                                    Log::debug('  Changing is_linked to FALSE for ' . $metafile->path);
                                }
                                $metafile->is_linked = FALSE;
                                $metafiles[$key] = $metafile;
                                static::save_metafiles($share, $path, $filename, $metafiles);
                            } else if (empty($metafile->is_linked) && $metafile->path == $share_file_link_to) {
                                if (!$quiet) {
                                    Log::debug('  Changing is_linked to TRUE for ' . $metafile->path);
                                }
                                $metafile->is_linked = TRUE;
                                $metafiles[$key] = $metafile;
                                static::save_metafiles($share, $path, $filename, $metafiles);
                            }
                        }
                    }
                }
            }
        }
        $ok_metafiles = array();
        foreach ($metafiles as $key => $metafile) {
            $valid_path = FALSE;
            $drive = StoragePool::getDriveFromPath($metafile->path);
            if ($drive !== FALSE) {
                $valid_path = TRUE;
            }
            if ($valid_path && ($load_nok_metafiles || $metafile->state == Metafile::STATE_OK)) {
                $key = clean_dir($metafile->path);
                if (isset($ok_metafiles[$key])) {
                    $previous_metafile = $ok_metafiles[$key];
                    if ($previous_metafile->state == Metafile::STATE_OK && $metafile->state != Metafile::STATE_OK) {
                        continue;
                    }
                }
                $ok_metafiles[$key] = $metafile;
            } else {
                if (!$valid_path && $metafile->state != Metafile::STATE_GONE) {
                    Log::warn("Found a metadata file pointing to a drive not defined in your storage pool: '$metafile->path'. Will mark it as Gone.", Log::EVENT_CODE_METADATA_POINTS_TO_GONE_DRIVE);
                    $metafile->state = Metafile::STATE_GONE;
                    $metafiles[$key] = $metafile;
                    static::save_metafiles($share, $path, $filename, $metafiles);
                }
            }
        }
        $metafiles = $ok_metafiles;
        if (!$quiet) {
            Log::debug("  Got " . count($metafiles) . " metadata files.");
        }
        return $metafiles;
    }
    public static function create_metafiles($share, $full_path, $num_copies_required, $filesize, $metafiles=[]) {
        $found_link_metafile = FALSE;
        list($path, ) = explode_full_path($full_path);
        $num_ok = count($metafiles);
        foreach ($metafiles as $key => $metafile) {
            $sp_drive = str_replace(clean_dir("/$share/$full_path"), '', $metafile->path);
            if (!StoragePool::is_pool_drive($sp_drive)) {
                $metafile->state = Metafile::STATE_GONE;
            }
            $df = StoragePool::get_free_space($sp_drive);
            if (!$df) {
                $free_space = 0;
            } else {
                $free_space = $df['free'];
            }
            if ($free_space <= $filesize/1024) {
                $metafile->state = Metafile::STATE_GONE;
            }
            if ($metafile->state != Metafile::STATE_OK && $metafile->state != Metafile::STATE_PENDING) {
                $num_ok--;
            }
            if ($key != $metafile->path) {
                unset($metafiles[$key]);
                $key = $metafile->path;
            }
            if ($metafile->is_linked) {
                $found_link_metafile = TRUE;
            }
            $metafiles[$key] = $metafile;
        }
        if ($num_ok < $num_copies_required) {
            $target_drives = StoragePool::choose_target_drives($filesize/1024, FALSE, $share, $path, '  ');
        }
        while ($num_ok < $num_copies_required && count($target_drives) > 0) {
            $sp_drive = array_shift($target_drives);
            $clean_target_full_path = clean_dir("$sp_drive/$share/$full_path");
            if (isset($metafiles[$clean_target_full_path])) {
                continue;
            }
            foreach ($metafiles as $metafile) {
                if ($clean_target_full_path == clean_dir($metafile->path)) {
                    continue;
                }
            }
            $metafiles = array_reverse($metafiles);
            $metafiles[$clean_target_full_path] = (object) array('path' => $clean_target_full_path, 'is_linked' => FALSE, 'state' => Metafile::STATE_PENDING);
            $metafiles = array_reverse($metafiles);
            $num_ok++;
        }
        if (!$found_link_metafile) {
            foreach ($metafiles as $metafile) {
                $metafile->is_linked = TRUE;
                break;
            }
        }
        return $metafiles;
    }
    public static function save_metafiles($share, $path, $filename, $metafiles) {
        if (count($metafiles) == 0) {
            static::remove_metafiles($share, $path, $filename);
            return;
        }
        $metafiles = array_values($metafiles);
        Log::debug("  Saving " . count($metafiles) . " metadata files for " . clean_dir($share . (!empty($path) ? "/$path" : "") . ($filename!== null ? "/$filename" : "")));
        $paths_used = array();
        foreach (static::get_metastores() as $metastore) {
            $sp_drive = str_replace('/' . static::METASTORE_DIR, '', $metastore);
            $data_filepath = clean_dir("$metastore/$share/$path");
            $has_metafile = FALSE;
            foreach ($metafiles as $metafile) {
                if (StoragePool::getDriveFromPath($metafile->path) == $sp_drive && StoragePool::is_pool_drive($sp_drive)) {
                    gh_mkdir($data_filepath, get_share_landing_zone($share) . "/$path");
                    if (is_dir("$data_filepath/$filename")) {
                        exec("rm -rf " . escapeshellarg("$data_filepath/$filename"));
                    }
                    $worked = @file_put_contents("$data_filepath/$filename", serialize($metafiles));
                    if ($worked === FALSE) {
                        $worked = @file_put_contents(normalize_utf8_characters("$data_filepath/$filename"), serialize($metafiles));
                        if ($worked !== FALSE) {
                            $data_filepath = normalize_utf8_characters($data_filepath);
                            $filename = normalize_utf8_characters($filename);
                        } else {
                            Log::warn("  Failed to save metadata file in $data_filepath/$filename", Log::EVENT_CODE_NO_METADATA_SAVED);
                        }
                    }
                    $has_metafile = TRUE;
                    $paths_used[] = $data_filepath;
                    break;
                }
            }
            if (!$has_metafile && file_exists("$data_filepath/$filename")) {
                if (is_dir("$data_filepath/$filename")) {
                    rmdir("$data_filepath/$filename");
                } else {
                    unlink("$data_filepath/$filename");
                }
            }
        }
        if (count($paths_used) == 1) {
            $metastore_backup_drives = Config::get(CONFIG_METASTORE_BACKUPS);
            if (!empty($metastore_backup_drives)) {
                if (!string_contains($paths_used[0], str_replace(static::METASTORE_BACKUP_DIR, static::METASTORE_DIR, $metastore_backup_drives[0]))) {
                    $metastore_backup_drive = $metastore_backup_drives[0];
                } else {
                    $metastore_backup_drive = $metastore_backup_drives[1];
                }
                $data_filepath = "$metastore_backup_drive/$share/$path";
                Log::debug("    Saving backup metadata file in $data_filepath/$filename");
                if (gh_mkdir($data_filepath, get_share_landing_zone($share) . "/$path")) {
                    if (!@file_put_contents("$data_filepath/$filename", serialize($metafiles))) {
                        Log::warn("  Failed to save backup metadata file in $data_filepath/$filename", Log::EVENT_CODE_NO_METADATA_SAVED);
                    }
                }
            }
        }
    }
    public static function remove_metafiles($share, $path, $filename) {
        Log::debug("  Removing metadata files for $share" . (!empty($path) && $path != '.' ? "/$path" : "") . ($filename!== null ? "/$filename" : ""));
        foreach (static::get_metafile_data_filenames($share, $path, $filename) as $f) {
            @unlink($f);
            Log::debug("    Removed metadata file at $f");
            clearstatcache();
        }
    }
}
class Metafile extends stdClass {
    const STATE_OK = 'OK';
    const STATE_GONE = 'Gone';
    const STATE_PENDING = 'Pending';
    public $path;
    public $state;
    public $is_linked;
}
class metafile_iterator implements Iterator {
    private $path;
    private $share;
    private $load_nok_metafiles;
    private $quiet;
    private $check_symlink;
    private $metafiles;
    private $metastores;
    private $dir_handle;
    private $directory_stack;
    public function __construct($share, $path, $load_nok_metafiles=FALSE, $quiet=FALSE, $check_symlink=TRUE) {
        $this->quiet = $quiet;
        $this->share = $share;
        $this->path = $path;
        $this->check_symlink = $check_symlink;
        $this->load_nok_metafiles = $load_nok_metafiles;
    }
    #[\ReturnTypeWillChange]
    public function rewind() {
        $this->metastores = Metastores::get_metastores();
        $this->directory_stack = array($this->path);
        $this->dir_handle = NULL;
        $this->metafiles = array();
        $this->next();
    }
    #[\ReturnTypeWillChange]
    public function current() {
        return $this->metafiles;
    }
    #[\ReturnTypeWillChange]
    public function key() {
        return count($this->metafiles);
    }
    #[\ReturnTypeWillChange]
    public function next() {
        $this->metafiles = array();
        while (count($this->directory_stack) > 0 && $this->directory_stack !== NULL) {
            $dir = array_pop($this->directory_stack);
            if (!$this->quiet) {
                Log::debug("Loading metadata files for (dir) " . clean_dir($this->share . (!empty($dir) ? "/" . $dir : "")) . " ...");
            }
            for ($i = 0; $i < count($this->metastores); $i++) {
                $metastore = $this->metastores[$i];
                $base = "$metastore/" . $this->share . "/";
                if (!file_exists($base . $dir)) {
                    continue;
                }
                if ($this->dir_handle = opendir($base . $dir)) {
                    while (false !== ($file = readdir($this->dir_handle))) {
                        memory_check();
                        if ($file=='.' || $file=='..') {
                            continue;
                        }
                        if (!empty($dir)) {
                            $full_filename = $dir . '/' . $file;
                        } else {
                            $full_filename = $file;
                        }
                        if (is_dir($base . $full_filename)) {
                            $this->directory_stack[] = $full_filename;
                        } else {
                            $full_filename = str_replace("$this->path/",'',$full_filename);
                            if (isset($this->metafiles[$full_filename])) {
                                continue;
                            }
                            $this->metafiles[$full_filename] = Metastores::get_metafiles_for_file($this->share, $dir, $file, $this->load_nok_metafiles, $this->quiet, $this->check_symlink);
                        }
                    }
                    closedir($this->dir_handle);
                    $this->directory_stack = array_unique($this->directory_stack);
                }
            }
            if (count($this->metafiles) > 0) {
                break;
            }
        }
        if (!$this->quiet) {
            Log::debug('Found ' . count($this->metafiles) . ' metadata files.');
        }
        return $this->metafiles;
    }
    #[\ReturnTypeWillChange]
    public function valid() {
        return count($this->metafiles) > 0;
    }
}

final class MigrationHelper {
    public static function terminologyConversion() {
        self::convertFolders('.gh_graveyard', Metastores::METASTORE_DIR);
        self::convertFolders('.gh_graveyard_backup', Metastores::METASTORE_BACKUP_DIR);
        self::convertFolders('.gh_attic', '.gh_trash');
        self::convertDatabase();
        self::convertStoragePoolDrivesTagFiles();
    }
	private static function convertFolders($old, $new) {
        foreach (Config::storagePoolDrives() as $sp_drive) {
			$old_term = clean_dir("$sp_drive/$old");
			$new_term = clean_dir("$sp_drive/$new");
			if (file_exists($old_term)) {
				Log::info("Moving $old_term to $new_term...");
				gh_rename($old_term, $new_term);
			}
		}
	}
	private static function convertDatabase() {
        Settings::rename('graveyard_backup_directory', 'metastore_backup_directory');
        $setting = Settings::get('metastore_backup_directory', FALSE, '%graveyard%');
        if ($setting) {
            $new_value = str_replace('/.gh_graveyard_backup', '/' . Metastores::METASTORE_BACKUP_DIR, $setting);
            Settings::set('metastore_backup_directory', $new_value);
        }
	}
    public static function convertStoragePoolDrivesTagFiles() {
        global $going_drive;
        $drives_definitions = Settings::get('sp_drives_definitions', TRUE);
        if (!$drives_definitions) {
            $drives_definitions = array();
        }
        foreach (Config::storagePoolDrives() as $sp_drive) {
            if (isset($going_drive) && $sp_drive == $going_drive) { continue; }
            $drive_uuid = SystemHelper::directory_uuid($sp_drive);
            if (!isset($drives_definitions[$sp_drive])) {
                if (is_dir($sp_drive)) {
                    $drives_definitions[$sp_drive] = $drive_uuid;
                }
            }
            if (!isset($drives_definitions[$sp_drive])) {
                continue;
            }
            if ($drives_definitions[$sp_drive] === FALSE) {
                unset($drives_definitions[$sp_drive]);
                continue;
            }
            if (file_exists("$sp_drive/.greyhole_uses_this") && $drive_uuid != 'remote') {
                unlink("$sp_drive/.greyhole_uses_this");
            }
            if ($drives_definitions[$sp_drive] != $drive_uuid) {
                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 --replaced'. Because of that, Greyhole will NOT use this drive at this time.", Log::EVENT_CODE_STORAGE_POOL_DRIVE_UUID_CHANGED);
            }
        }
        foreach ($drives_definitions as $sp_drive => $uuid) {
            if (!array_contains(Config::storagePoolDrives(), $sp_drive)) {
                unset($drives_definitions[$sp_drive]);
            }
        }
        $devices = array();
        foreach ($drives_definitions as $sp_drive => $device_id) {
            $devices[$device_id][] = $sp_drive;
        }
        foreach ($devices as $device_id => $sp_drives) {
            if (count($sp_drives) > 1 && $device_id !== 0 && $device_id != 'remote') {
                if (Config::get(CONFIG_ALLOW_MULTIPLE_SP_PER_DRIVE)) {
                    Log::info("The following storage pool drives are on the same partition: " . implode(", ", $sp_drives) . ", but per greyhole.conf '" . CONFIG_ALLOW_MULTIPLE_SP_PER_DRIVE . "' options, you chose to ignore this normally critical error.");
                } else {
                    Log::critical("ERROR: The following storage pool drives are on the same partition: " . implode(", ", $sp_drives) . ". The Greyhole daemon will now stop.", Log::EVENT_CODE_CONFIG_STORAGE_POOL_DRIVES_SAME_PARTITION);
                }
            }
        }
        Settings::set('sp_drives_definitions', $drives_definitions);
        return $drives_definitions;
	}
}

class PoolDriveSelector {
    var $num_drives_per_draft;
    var $selection_algorithm;
    var $drives;
    var $is_forced;
    var $group_name;
    var $num_drives_config;
    var $sorted_target_drives;
    var $last_resort_sorted_target_drives;
    function __construct($num_drives_per_draft, $selection_algorithm, $drives, $is_forced, $group_name, $num_drives_config) {
        $this->num_drives_per_draft = $num_drives_per_draft;
        $this->selection_algorithm = $selection_algorithm;
        $this->drives = $drives;
        $this->is_forced = $is_forced;
        $this->group_name = $group_name;
        $this->num_drives_config = $num_drives_config;
    }
    public function isForced() {
        return $this->is_forced;
    }
    function init(&$sorted_target_drives, &$last_resort_sorted_target_drives) {
        if ($this->selection_algorithm == 'least_used_space') {
            $sorted_target_drives = $sorted_target_drives['used_space'];
            $last_resort_sorted_target_drives = $last_resort_sorted_target_drives['used_space'];
            asort($sorted_target_drives);
            asort($last_resort_sorted_target_drives);
        } else if ($this->selection_algorithm == 'most_available_space') {
            $sorted_target_drives = $sorted_target_drives['available_space'];
            $last_resort_sorted_target_drives = $last_resort_sorted_target_drives['available_space'];
            arsort($sorted_target_drives);
            arsort($last_resort_sorted_target_drives);
        } else {
            Log::critical("Unknown '" . CONFIG_DRIVE_SELECTION_ALGORITHM . "' found: " . $this->selection_algorithm, Log::EVENT_CODE_CONFIG_INVALID_VALUE);
        }
        $this->sorted_target_drives = array();
        foreach ($sorted_target_drives as $sp_drive => $space) {
            if (array_contains($this->drives, $sp_drive)) {
                $this->sorted_target_drives[$sp_drive] = $space;
            }
        }
        $this->last_resort_sorted_target_drives = array();
        foreach ($last_resort_sorted_target_drives as $sp_drive => $space) {
            if (array_contains($this->drives, $sp_drive)) {
                $this->last_resort_sorted_target_drives[$sp_drive] = $space;
            }
        }
    }
    function draft() {
        $drives = array();
        $drives_last_resort = array();
        while (count($drives)<$this->num_drives_per_draft) {
            $arr = kshift($this->sorted_target_drives);
            if ($arr === FALSE) {
                break;
            }
            list($sp_drive, $space) = $arr;
            if (!StoragePool::is_pool_drive($sp_drive)) { continue; }
            $drives[$sp_drive] = $space;
        }
        while (count($drives)+count($drives_last_resort)<$this->num_drives_per_draft) {
            $arr = kshift($this->last_resort_sorted_target_drives);
            if ($arr === FALSE) {
                break;
            }
            list($sp_drive, $space) = $arr;
            if (!StoragePool::is_pool_drive($sp_drive)) { continue; }
            $drives_last_resort[$sp_drive] = $space;
        }
        return array($drives, $drives_last_resort);
    }
    static function parse($config_string, $drive_selection_groups) {
        $ds = array();
        if ($config_string == 'least_used_space' || $config_string == 'most_available_space') {
            $ds[] = new PoolDriveSelector(count(Config::storagePoolDrives()), $config_string, Config::storagePoolDrives(), FALSE, 'all', 'all');
            return $ds;
        }
        if (!preg_match('/forced ?\((.+)\) ?(least_used_space|most_available_space)/i', $config_string, $regs)) {
            Log::critical("Can't understand the '" . CONFIG_DRIVE_SELECTION_ALGORITHM . "' value: $config_string", Log::EVENT_CODE_CONFIG_INVALID_VALUE);
        }
        $selection_algorithm = $regs[2];
        $groups = array_map('trim', explode(',', $regs[1]));
        foreach ($groups as $group) {
            $group = explode(' ', preg_replace('/^([0-9]+)x/', '\\1 ', $group));
            $num_drives_config = trim($group[0]);
            $group_name = trim($group[1]);
            if (!isset($drive_selection_groups[$group_name])) {
                continue;
            }
            if (stripos($num_drives_config, 'all') === 0 || $num_drives_config > count($drive_selection_groups[$group_name])) {
                $num_drives = count($drive_selection_groups[$group_name]);
            } else {
                $num_drives = $num_drives_config;
            }
            $ds[] = new PoolDriveSelector($num_drives, $selection_algorithm, $drive_selection_groups[$group_name], TRUE, $group_name, $num_drives_config);
        }
        return $ds;
    }
    function update() {
        if (!$this->is_forced && ($this->selection_algorithm == 'least_used_space' || $this->selection_algorithm == 'most_available_space')) {
            $this->num_drives_per_draft = count(Config::storagePoolDrives());
            $this->drives = Config::storagePoolDrives();
        }
    }
}

final class SambaSpool {
    public static function create_mem_spool() {
        $mounted_already = exec('mount | grep /var/spool/greyhole/mem | wc -l');
        if (!$mounted_already && file_exists('/var/spool/greyhole/mem')) {
            exec("cat /proc/1/sched | grep supervisord", $output, $result);
            $is_docker = ($result === 0);
            if ($is_docker) {
                $output = exec("df /var/spool/greyhole/mem | tail -1");
                $mounted_already = preg_match('/^none /', $output);
            }
        }
        if (!$mounted_already) {
            if (!file_exists('/var/spool/greyhole/mem')) {
                mkdir('/var/spool/greyhole/mem', 0777, TRUE);
                chmod('/var/spool/greyhole/mem', 0777); // mkdir mode is affected by the umask, so we need to insure proper mode on that folder.
            }
            exec('mount -o size=4M -t tmpfs none /var/spool/greyhole/mem 2> /dev/null', $mount_result);
            if (!empty($mount_result)) {
                Log::error("Error mounting tmpfs in /var/spool/greyhole/mem: $mount_result", Log::EVENT_CODE_SPOOL_MOUNT_FAILED);
            }
            return TRUE;
        }
        return FALSE;
    }
    public static function parse_samba_spool() {
        Log::setAction(ACTION_READ_SAMBA_POOL);
        $db_spool = DBSpool::getInstance();
        if (!file_exists('/var/spool/greyhole/mem')) {
            mkdir('/var/spool/greyhole/mem', 0777, TRUE);
            chmod('/var/spool/greyhole', 0777); // mkdir mode is affected by the umask, so we need to insure proper mode on that folder.
            chmod('/var/spool/greyhole/mem', 0777);
        }
        if (!DB::acquireLock(ACTION_READ_SAMBA_POOL, 5)) {
            return;
        }
        $new_tasks = 0;
        $last_line = FALSE;
        $act = FALSE;
        $close_tasks = array();
        while (TRUE) {
            $files = array();
            $last_filename = FALSE;
            exec('find -L /var/spool/greyhole -type f -printf "%T@ %p\n" | sort -n 2> /dev/null | head -n 10000', $files);
            if (count($files) == 0) {
                break;
            }
            if ($last_line === FALSE) {
                Log::debug("Processing Samba spool...");
            }
            $fct_sort_filename = function ($file1, $file2) {
                $file1 = explode(' ', $file1);
                $ts1 = array_shift($file1);
                $file1 = implode(' ', $file1);
                $file2 = explode(' ', $file2);
                $ts2 = array_shift($file2);
                $file2 = implode(' ', $file2);
                list($ts1p1, $ts1p2) = explode('.', $ts1);
                list($ts2p1, $ts2p2) = explode('.', $ts2);
                $ts1p1 = (int) $ts1p1;
                $ts1p2 = (int) $ts1p2;
                $ts2p1 = (int) $ts2p1;
                $ts2p2 = (int) $ts2p2;
                if ($ts1p1 < $ts2p1) {
                    return -1;
                }
                if ($ts1p1 > $ts2p1) {
                    return 1;
                }
                if ($ts1p2 < $ts2p2) {
                    return -1;
                }
                if ($ts1p2 > $ts2p2) {
                    return 1;
                }
                $is_file1_write = string_starts_with($file1, '/var/spool/greyhole/mem/');
                $is_file2_write = string_starts_with($file2, '/var/spool/greyhole/mem/');
                $bfile1 = basename($file1);
                $bfile2 = basename($file2);
                $ts1 = explode('-', $bfile1)[0];
                $ts2 = explode('-', $bfile2)[0];
                $seconds1 = substr($ts1, 0, 10);
                $seconds2 = substr($ts2, 0, 10);
                $useconds1 = substr($ts1, -6);
                $useconds2 = substr($ts2, -6);
                if ($seconds1 < $seconds2) {
                    return -1;
                }
                if ($seconds1 > $seconds2) {
                    return 1;
                }
                if ($is_file1_write && $is_file2_write) {
                    return 0;
                }
                if (!$is_file1_write && !$is_file2_write) {
                    if ($useconds1 < $useconds2) {
                        return -1;
                    }
                    return 1;
                }
                if ($is_file1_write && !$is_file2_write) {
                    $other_file = $file2;
                } else {
                    $other_file = $file1;
                }
                $log = file_get_contents($other_file);
                if (string_starts_with($log, 'open')) {
                    return $is_file1_write ? 1 : -1; // open before write
                }
                if (string_starts_with($log, 'close')) {
                    return $is_file1_write ? -1 : 1; // close after write
                }
                return 0;
            };
            usort($files, $fct_sort_filename);
            foreach ($files as $file) {
                $file = explode(' ', $file);
                array_shift($file);
                $filename = implode(' ', $file);
                if ($last_filename) {
                    unlink($last_filename);
                }
                $last_filename = $filename;
                $line = file_get_contents($filename);
                if ($line === $last_line) {
                    continue;
                }
                $line_ar = explode("\n", $line);
                $last_line = $line;
                if ($act === 'fwrite' || $act === 'close') {
                    $db_spool->close_task($act, $share, $fd, @$fullpath, $close_tasks);
                }
                $line = $line_ar;
                $act = array_shift($line);
                $share = array_shift($line);
                if ($act == 'mkdir') {
                    if (!empty($line[1])) {
                        $path = str_replace(get_share_landing_zone($share) . '/', '', $line[1] . "/" . $line[0]);
                    } else {
                        $path = $line[0];
                    }
                    $dir_fullpath = get_share_landing_zone($share) . "/" . $path;
                    Log::debug("Directory created: $share/$path");
                    foreach (Config::get(CONFIG_METASTORE_BACKUPS) as $metastore_backup_drive) {
                        $backup_drive = str_replace('/' . Metastores::METASTORE_BACKUP_DIR, '', $metastore_backup_drive);
                        if (StoragePool::is_pool_drive($backup_drive)) {
                            gh_mkdir("$backup_drive/$share/$path", $dir_fullpath);
                        }
                    }
                    FileHook::trigger(FileHook::EVENT_TYPE_MKDIR, $share, $path);
                    continue;
                }
                $result = array_pop($line);
                if (string_starts_with($result, 'failed')) {
                    Log::debug("Failed $act in $share/$line[0]. Skipping.");
                    continue;
                }
                unset($fullpath);
                unset($fullpath_target);
                unset($fd);
                switch ($act) {
                case 'open':
                    $fullpath = array_shift($line);
                    $fd = array_shift($line);
                    if (!empty($line)) {
                        array_shift($line); // 'for writing'
                    }
                    if (!empty($line[0])) {
                        $fullpath = str_replace(get_share_landing_zone($share) . '/', '', array_shift($line));
                    }
                    $act = 'write';
                    break;
                case 'rmdir':
                case 'unlink':
                    $fullpath = array_shift($line);
                    break;
                case 'rename':
                case 'link':
                    $fullpath = array_shift($line);
                    $fullpath_target = array_shift($line);
                    break;
                case 'fwrite':
                case 'close':
                    $fd = array_shift($line);
                    if (!empty($line)) {
                        $fullpath = array_shift($line);
                    }
                    if (empty($fullpath)) {
                        $fullpath = NULL;
                    }
                    break;
                default:
                    $act = FALSE;
                }
                if ($act === FALSE) {
                    continue;
                }
                if ($act != 'close' && $act != 'fwrite') {
                    if (isset($fd) && $fd == -1) {
                        continue;
                    }
                    if ($act != 'unlink' && $act != 'rmdir' && array_contains(ConfigHelper::$trash_share_names, $share)) { continue; }
                    $new_tasks++;
                    $db_spool->insert($act, $share, @$fullpath, @$fullpath_target, @$fd);
                }
            }
            if ($last_filename) {
                unlink($last_filename);
            }
        }
        if ($act === 'fwrite' || $act === 'close') {
            $db_spool->close_task($act, $share, $fd, @$fullpath, $close_tasks);
        }
        Log::perf("Finished parsing spool.");
        if (!empty($close_tasks)) {
            Log::perf("Found " . count($close_tasks) . " close tasks. Will finalize all write tasks for those, if any...");
            $db_spool->close_all_tasks($close_tasks);
        }
        if ($new_tasks > 0) {
            Log::debug("Found $new_tasks new tasks in spool.");
        }
        DB::releaseLock(ACTION_READ_SAMBA_POOL);
        Log::restorePreviousAction();
    }
}

final class SambaUtils {
    public static function samba_get_version() {
        return str_replace(' ', '.', exec('/usr/sbin/smbd --version | awk \'{print $2}\' | awk -F\'-\' \'{print $1}\' | awk -F\'.\' \'{print $1,$2}\''));
    }
    public static function get_smbd_pid() {
        $response = exec("/usr/bin/smbcontrol smbd ping");
        if (preg_match('/from pid (\d+)/', $response, $re)) {
            return (int) $re[1];
        }
        return FALSE;
    }
    public static function samba_restart() {
        Log::info("The Samba daemon will now restart...");
        if (is_file('/etc/init/smbd.conf')) {
            exec("/sbin/restart smbd");
        } else if (is_file('/etc/init.d/samba')) {
            exec("/etc/init.d/samba restart");
        } else if (is_file('/etc/init.d/smb')) {
            exec("/etc/init.d/smb restart");
        } else if (is_file('/etc/init.d/smbd')) {
            exec("/etc/init.d/smbd restart");
        } else if (is_file('/etc/systemd/system/multi-user.target.wants/smb.service')) {
            exec("systemctl restart smb.service");
        } else if (is_file('/usr/bin/supervisorctl')) {
            exec("/usr/bin/supervisorctl status smbd", $out, $return);
            if ($return === 0) {
                exec("/usr/bin/supervisorctl restart smbd");
            }
        } else {
            return FALSE;
        }
        return TRUE;
    }
    public static function samba_check_vfs() {
        $vfs_is_ok = FALSE;
        $version = str_replace('.', '', SambaUtils::samba_get_version());
        $arch = exec('uname -m');
        if (file_exists('/usr/lib/x86_64-linux-gnu/samba/vfs')) {
            $source_libdir = '/usr/lib64'; # Makefile will always install Greyhole .so files in /usr/lib64, for x86_64 CPUs. @see Makefile
            $target_libdir = '/usr/lib/x86_64-linux-gnu';
        } else if ($arch == "x86_64") {
            $source_libdir = '/usr/lib64';
            $target_libdir = '/usr/lib64';
            if (file_exists('/usr/lib/samba/vfs')) {
                $target_libdir = '/usr/lib';
            }
        } else if (file_exists('/usr/lib/aarch64-linux-gnu/samba/vfs')) {
            $source_libdir = '/usr/lib/aarch64-linux-gnu';
            $target_libdir = '/usr/lib/aarch64-linux-gnu';
        } else {
            $source_libdir = '/usr/lib';
            $target_libdir = '/usr/lib';
        }
        $vfs_file = "$target_libdir/samba/vfs/greyhole.so";
        Log::debug("Checking symlink at $vfs_file...");
        if (is_file($vfs_file)) {
            $vfs_target = @readlink($vfs_file);
            if (strpos($vfs_target, "/greyhole-samba$version.so") !== FALSE) {
                Log::debug("  Is OK.");
                $vfs_is_ok = TRUE;
            }
        }
        if (!$vfs_is_ok) {
            $vfs_target = "$source_libdir/greyhole/greyhole-samba$version.so";
            Log::warn("  Greyhole VFS module for Samba was missing, or the wrong version for your Samba. It will now be replaced with a symlink to $vfs_target", Log::EVENT_CODE_VFS_MODULE_WRONG);
            if (!is_file($vfs_target)) {
                if (!is_dir(dirname($vfs_target))) {
                    mkdir(dirname($vfs_target), 0755, TRUE);
                }
                $url = "https://github.com/gboudreau/Greyhole/raw/master/samba-module/bin/" . SambaUtils::samba_get_version() . "/greyhole-$arch.so";
                Log::info("  $vfs_target is missing; will download it from $url ...");
                file_put_contents($vfs_target, file_get_contents($url));
            }
            if (is_file($vfs_file) || is_link($vfs_file)) {
                unlink($vfs_file);
            }
            if (!is_dir(dirname($vfs_file))) {
                mkdir(dirname($vfs_file));
            }
            gh_symlink($vfs_target, $vfs_file);
            if (!static::samba_restart()) {
                Log::critical("Couldn't find how to restart Samba. Please restart the Samba daemon manually.", Log::EVENT_CODE_SAMBA_RESTART_FAILED);
            }
        }
        if (file_exists("$target_libdir/samba/libsmbd_base.so.0") && !file_exists("$target_libdir/samba/libsmbd_base.so")) {
            Log::info("  Ubuntu 14 bugfix: creating symlink pointing to libsmbd_base.so.0 as libsmbd_base.so.");
            gh_symlink("$target_libdir/samba/libsmbd_base.so.0", "$target_libdir/samba/libsmbd_base.so");
        }
        exec("ldd " . escapeshellarg($vfs_file) . " 2>/dev/null | grep 'not found'", $output);
        if (!empty($output)) {
            Log::warn("  Greyhole VFS module ($vfs_file) seems to be missing some required libraries. If you have issues connecting to your Greyhole-enabled shares, try to compile a new VFS module for Samba by running this command: sudo /usr/share/greyhole/build_vfs.sh current", Log::EVENT_CODE_VFS_MODULE_WRONG);
        }
    }
}

final class Settings {
    public static function get($name, $unserialize=FALSE, $value=FALSE) {
        if (!DB::isConnected()) {
            $setting = FALSE;
        } else {
            $query = "SELECT * FROM settings WHERE name LIKE :name";
            $params = array('name' => $name);
            if ($value !== FALSE) {
                $query .= " AND value LIKE :value";
                $params['value'] = $value;
            }
            $setting = DB::getFirst($query, $params);
        }
        if ($setting === FALSE) {
            return FALSE;
        }
        return $unserialize ? unserialize($setting->value) : $setting->value;
    }
    public static function set($name, $value) {
        if (is_array($value)) {
            $value = serialize($value);
        }
        $query = "INSERT INTO settings SET name = :name, value = :value ON DUPLICATE KEY UPDATE value = VALUES(value)";
        DB::insert($query, array('name' => $name, 'value' => $value));
        return (object) array('name' => $name, 'value' => $value);
    }
    public static function rename($from, $to) {
        $query = "UPDATE settings SET name = :to WHERE name = :from";
        DB::execute($query, array('from' => $from, 'to' => $to));
    }
    public static function backup() {
        $settings = DB::getAll("SELECT * FROM settings");
        foreach (Config::storagePoolDrives() as $sp_drive) {
            if (StoragePool::is_pool_drive($sp_drive)) {
                $settings_backup_file = "$sp_drive/.gh_settings.bak";
                file_put_contents($settings_backup_file, serialize($settings));
            }
        }
    }
    public static function restore() {
        $latest_backup_time = 0;
        foreach (Config::storagePoolDrives() as $sp_drive) {
            $settings_backup_file = "$sp_drive/.gh_settings.bak";
            if (file_exists($settings_backup_file)) {
                $last_mod_date = filemtime($settings_backup_file);
                if ($last_mod_date > $latest_backup_time) {
                    $backup_file = $settings_backup_file;
                    $latest_backup_time = $last_mod_date;
                }
            }
        }
        if (isset($backup_file)) {
            Log::info("Restoring settings from last backup: $backup_file");
            $settings = unserialize(file_get_contents($backup_file));
            foreach ($settings as $setting) {
                Settings::set($setting->name, $setting->value);
            }
            return TRUE;
        }
        return FALSE;
    }
}

final class StorageFile {
    public static function create_file_copies_from_metafiles($metafiles, $share, $full_path, $source_file, $missing_only = FALSE) {
        $landing_zone = get_share_landing_zone($share);
        list($path, $filename) = explode_full_path($full_path);
        $source_file = clean_dir($source_file);
        $file_copies_to_create = [];
        foreach ($metafiles as $key => $metafile) {
            if (!Log::actionIs(ACTION_CP) && !gh_file_exists("$landing_zone/$full_path", '  $real_path doesn\'t exist anymore. Aborting.')) {
                return FALSE;
            }
            if ($metafile->path == $source_file && $metafile->state == Metafile::STATE_OK && gh_filesize($metafile->path) == gh_filesize($source_file)) {
                Log::debug("  File copy at $metafile->path is already up to date.");
                continue;
            }
            if ($missing_only && gh_file_exists($metafile->path) && $metafile->state == Metafile::STATE_OK && gh_filesize($metafile->path) == gh_filesize($source_file)) {
                Log::debug("  File copy at $metafile->path is already up to date.");
                continue;
            }
            $root_path = str_replace(clean_dir("/$share/$full_path"), '', $metafile->path);
            if (!StoragePool::is_pool_drive($root_path)) {
                Log::warn("  Warning! It seems the partition UUID of $root_path changed. This probably means this mount is currently unmounted, or that you replaced this drive and didn't use 'greyhole --replaced'. Because of that, Greyhole will NOT use this drive at this time.", Log::EVENT_CODE_STORAGE_POOL_DRIVE_UUID_CHANGED);
                $metafile->state = Metafile::STATE_GONE;
                $metafiles[$key] = $metafile;
                continue;
            }
            list($metafile_dir_path, ) = explode_full_path($metafile->path);
            list($original_path, ) = explode_full_path(get_share_landing_zone($share) . "/$full_path");
            if (!gh_mkdir($metafile_dir_path, $original_path)) {
                $metafile->state = Metafile::STATE_GONE;
                $metafiles[$key] = $metafile;
                continue;
            }
            $file_copies_to_create[$key] = $metafile;
        }
        $create_copies_in_parallel = count($file_copies_to_create) > 1 && !DBSpool::isCurrentTaskRetry() && Config::get(CONFIG_PARALLEL_COPYING);
        if ($create_copies_in_parallel) {
            $copy_results = static::create_file_copies($source_file, $file_copies_to_create);
        } else {
            $copy_results = [];
        }
        foreach ($file_copies_to_create as $key => $metafile) {
            $need_create_copy = empty($copy_results[$key]);
            if ($need_create_copy) {
                $copy_results[$key] = static::create_file_copy($source_file, $metafile->path);
            }
        }
        $link_next = FALSE;
        foreach ($file_copies_to_create as $key => $metafile) {
            $it_worked = !empty($copy_results[$key]);
            if (!$it_worked) {
                if ($metafile->is_linked) {
                    $metafile->is_linked = FALSE;
                    $link_next = TRUE;
                    if (@readlink("$landing_zone/$full_path") == $metafile->path) {
                        Log::debug("  Deleting symlink from landing zone, before recycling the file copy it points to.");
                        unlink("$landing_zone/$full_path");
                    }
                }
                $metafile->state = Metafile::STATE_GONE;
                Trash::trash_file($metafile->path);
                $metafiles[$key] = $metafile;
                Metastores::save_metafiles($share, $path, $filename, $metafiles);
                if (file_exists("$landing_zone/$full_path")) {
                    if (DBSpool::isCurrentTaskRetry()) {
                        Log::error("    Failed file copy (cont). We already retried this task. Aborting.", Log::EVENT_CODE_FILE_COPY_FAILED);
                        return FALSE;
                    }
                    Log::warn("    Failed file copy (cont). Will try to re-process this write task, since the source file seems intact.", Log::EVENT_CODE_FILE_COPY_FAILED);
                    DBSpool::setNextTask(
                        (object) array(
                            'id' => 0,
                            'action' => 'write',
                            'share' => $share,
                            'full_path' => clean_dir($full_path),
                            'complete' => 'yes'
                        )
                    );
                    return FALSE;
                }
                continue;
            }
            if ($link_next && !$metafile->is_linked) {
                $metafile->is_linked = TRUE;
            }
            $link_next = FALSE;
            if ($metafile->is_linked) {
                Log::debug("  Creating symlink in share pointing to $metafile->path");
                if (!is_dir("$landing_zone/$path/")) {
                    gh_mkdir("$landing_zone/$path/", dirname($source_file));
                }
                gh_symlink($metafile->path, "$landing_zone/$path/.gh_$filename");
                if (!file_exists("$landing_zone/$full_path") || unlink("$landing_zone/$full_path")) {
                    gh_rename("$landing_zone/$path/.gh_$filename", "$landing_zone/$path/$filename");
                } else {
                    unlink("$landing_zone/$path/.gh_$filename");
                }
            }
            if (gh_file_exists($metafile->path, '  Copy at $real_path doesn\'t exist. Will not mark it OK!')) {
                $metafile->state = Metafile::STATE_OK;
            }
            $metafiles[$key] = $metafile;
            if (!$create_copies_in_parallel) {
                Metastores::save_metafiles($share, $path, $filename, $metafiles);
            }
        }
        if ($create_copies_in_parallel) {
            Metastores::save_metafiles($share, $path, $filename, $metafiles);
        }
        return TRUE;
    }
    public static function create_file_copies($source_file, &$metafiles) {
        $copy_results = [];
        $copy_source = is_link($source_file) ? readlink($source_file) : $source_file;
        $source_size = gh_filesize($copy_source);
        $original_file_infos = StorageFile::get_file_permissions($copy_source);
        $file_copies_to_create = [];
        $tmp_file_copies_to_create = [];
        foreach ($metafiles as $key => $metafile) {
            $destination_file = $metafile->path;
            if (gh_is_file($source_file)) {
                if ($source_file == $destination_file) {
                    Log::debug("  Destination $destination_file is the same as the source. Nothing to do here; this file copy is ready!");
                    $copy_results[$key] = TRUE;
                    continue;
                }
                $source_dev = gh_file_deviceid($source_file);
                $target_dev = gh_file_deviceid(dirname($destination_file));
                if ($source_dev === $target_dev && $source_dev !== FALSE && !Config::get(CONFIG_ALLOW_MULTIPLE_SP_PER_DRIVE)) {
                    Log::debug("  Destination $destination_file is on the same drive as the source. Will be moved into storage pool drive later.");
                    $copy_results[$key] = FALSE;
                    continue;
                }
            }
            $temp_path = static::get_temp_filename($destination_file);
            $file_copies_to_create[] = $destination_file;
            $tmp_file_copies_to_create[] = $temp_path;
        }
        if (isset($source_size)) {
            Log::info("  Copying " . bytes_to_human($source_size, FALSE) . " file to: " . implode(', ', $file_copies_to_create));
        } else {
            Log::info("  Copying file to: " . implode(', ', $file_copies_to_create));
        }
        $start_time = time();
        if (!empty($tmp_file_copies_to_create)) {
            $copy_cmd = "cat " . escapeshellarg($copy_source) . " | tee " . implode(' ' , array_map('escapeshellarg', $tmp_file_copies_to_create));
            if (Config::get(CONFIG_CALCULATE_MD5_DURING_COPY)) {
                $copy_cmd .= " | md5sum";
            }
            $out = exec($copy_cmd);
            if (Config::get(CONFIG_CALCULATE_MD5_DURING_COPY)) {
                $md5 = first(explode(' ', $out));
                Log::debug("    Copied file MD5 = $md5");
            }
        }
        $first = TRUE;
        foreach ($metafiles as $key => $metafile) {
            $destination_file = $metafile->path;
            $temp_path = static::get_temp_filename($destination_file);
            if (!array_contains($tmp_file_copies_to_create, $temp_path)) {
                continue;
            }
            $it_worked = file_exists($temp_path) && file_exists($source_file) && gh_filesize($temp_path) == $source_size;
            if (!$it_worked) {
                $it_worked = file_exists(normalize_utf8_characters($temp_path)) && file_exists($source_file) && gh_filesize($temp_path) == $source_size;
                if ($it_worked) {
                    $temp_path = normalize_utf8_characters($temp_path);
                    $destination_file = normalize_utf8_characters($destination_file);
                    $metafile->path = $destination_file;
                    $metafiles[$key] = $metafile;
                }
            }
            $copy_results[$key] = $it_worked;
            if ($it_worked) {
                if ($first) {
                    if (time() - $start_time > 0) {
                        $speed = number_format($source_size/1024/1024 / (time() - $start_time), 1);
                        Log::debug("    Copy created at $speed MBps.");
                    }
                    if (!empty($md5)) {
                        list($share, $full_path) = get_share_and_fullpath_from_realpath($copy_source);
                        log_file_checksum($share, $full_path, $md5);
                    }
                    $first = FALSE;
                }
                gh_rename($temp_path, $destination_file);
                static::set_file_permissions($destination_file, $original_file_infos);
            } else {
                Log::warn("    Failed file copy. Will mark this metadata file 'Gone'.", Log::EVENT_CODE_FILE_COPY_FAILED);
                @unlink($temp_path);
            }
        }
        return $copy_results;
    }
    public static function create_file_copy($source_file, &$destination_file, $expected_md5 = NULL, &$error = NULL) {
        if (gh_is_file($source_file) && $source_file == $destination_file) {
            Log::debug("  Destination $destination_file is the same as the source. Nothing to do here; this file copy is ready!");
            return TRUE;
        }
        $start_time = time();
        $source_size = gh_filesize($source_file);
        $temp_path = static::get_temp_filename($destination_file);
        if (is_link($source_file)) {
            $link_target = readlink($source_file);
            $source_size = gh_filesize($link_target);
        } else if (gh_is_file($source_file)) {
            $source_size = gh_filesize($source_file);
        }
        if (isset($source_size)) {
            Log::info("  Copying " . bytes_to_human($source_size, FALSE) . " file to $destination_file");
        } else {
            Log::info("  Copying file to $destination_file");
        }
        $renamed = FALSE;
        if (gh_is_file($source_file)) {
            $source_dev = gh_file_deviceid($source_file);
            $target_dev = gh_file_deviceid(dirname($destination_file));
            if ($source_dev === $target_dev && $source_dev !== FALSE && !Config::get(CONFIG_ALLOW_MULTIPLE_SP_PER_DRIVE) && !Log::actionIs(ACTION_CP)) {
                Log::debug("  (using rename)");
                $original_file_infos = StorageFile::get_file_permissions($source_file);
                gh_rename($source_file, $temp_path);
                $renamed = TRUE;
            }
        }
        if (!$renamed) {
            $copy_source = is_link($source_file) ? readlink($source_file) : $source_file;
            $original_file_infos = StorageFile::get_file_permissions($copy_source);
            $copy_cmd = "cat " . escapeshellarg($copy_source) . " | tee " . escapeshellarg($temp_path);
            if (Config::get(CONFIG_CALCULATE_MD5_DURING_COPY) || !empty($expected_md5)) {
                $copy_cmd .= " | md5sum";
            }
            $out = exec($copy_cmd);
            if (Config::get(CONFIG_CALCULATE_MD5_DURING_COPY) || !empty($expected_md5)) {
                $md5 = first(explode(' ', $out));
                Log::debug("    Copied file MD5 = $md5");
                if (!empty($expected_md5)) {
                    if ($md5 != $expected_md5) {
                        Log::warn("    MD5 mismatch (expected $expected_md5). Failed file copy. Will mark this metadata file 'Gone'.", Log::EVENT_CODE_FILE_COPY_FAILED);
                        $error = "MD5 mismatch: expected $expected_md5, got $md5";
                        return FALSE;
                    } else {
                        Log::debug("    MD5 match expected value.");
                    }
                }
            }
        }
        $it_worked = file_exists($temp_path) && ($renamed || file_exists($source_file)) && gh_filesize($temp_path) == $source_size;
        if (!$it_worked) {
            $it_worked = file_exists(normalize_utf8_characters($temp_path)) && ($renamed || file_exists($source_file)) && gh_filesize($temp_path) == $source_size;
            if ($it_worked) {
                $temp_path = normalize_utf8_characters($temp_path);
                $destination_file = normalize_utf8_characters($destination_file);
            }
        }
        if ($it_worked) {
            if (time() - $start_time > 0) {
                $speed = number_format($source_size/1024/1024 / (time() - $start_time), 1);
                Log::debug("    Copy created at $speed MBps.");
            }
            gh_rename($temp_path, $destination_file);
            static::set_file_permissions($destination_file, $original_file_infos);
            if (!empty($md5)) {
                list($share, $full_path) = get_share_and_fullpath_from_realpath($destination_file);
                log_file_checksum($share, $full_path, $md5);
            }
        } else {
            if (!file_exists($temp_path)) {
                $error = "target file $temp_path doesn't exists";
            } elseif (gh_filesize($temp_path) != $source_size) {
                $error = "target filesize " . gh_filesize($temp_path) ." != source filesize $source_size";
            } else {
                $error = '?';
            }
            @Log::warn("    Failed file copy (failed check: $error). Will mark this metadata file 'Gone'.", Log::EVENT_CODE_FILE_COPY_FAILED);
            if ($renamed) {
                gh_rename($temp_path, $source_file);
            } else {
                @unlink($temp_path);
            }
        }
        return $it_worked;
    }
    public static function get_temp_filename($full_path) {
        list($path, $filename) = explode_full_path($full_path);
        return "$path/.$filename." . mb_substr(md5($filename), 0, 5);
    }
    public static function is_temp_file($full_path) {
        list(, $filename) = explode_full_path($full_path);
        if (preg_match("/^\.(.+)\.([0-9a-f]{5})$/", $filename, $regs)) {
            $md5_stem = mb_substr(md5($regs[1]), 0, 5);
            return ($md5_stem == $regs[2]);
        }
        return FALSE;
    }
    public static function set_file_permissions($real_file_path, $file_infos) {
        chmod($real_file_path, $file_infos->fileperms);
        chown($real_file_path, $file_infos->fileowner);
        chgrp($real_file_path, $file_infos->filegroup);
        touch($real_file_path, $file_infos->filemtime, time());
    }
    public static function get_file_permissions($real_path) {
        global $permissions_override;
        if ($real_path == null || !file_exists($real_path)) {
            return (object) array(
                'fileowner' => @$permissions_override['user'] ?: 0,
                'filegroup' => @$permissions_override['group'] ?: 0,
                'fileperms' => (int) base_convert("0777", 8, 10),
                'filemtime' => time()
            );
        }
        if (is_link($real_path)) {
            $real_path = realpath($real_path);
        }
        return (object) array(
            'fileowner' => (int) (@$permissions_override['user'] ?: gh_fileowner($real_path)),
            'filegroup' => (int) (@$permissions_override['group'] ?: gh_filegroup($real_path)),
            'fileperms' => (int) base_convert(gh_fileperms($real_path), 8, 10),
            'filemtime' => filemtime($real_path),
        );
    }
    public static function override_file_permissions() {
        global $permissions_override;
        if (@getmyuid() > 0) {
            $permissions_override['user'] = getmyuid();
            $permissions_override['group'] = getmygid();
        }
    }
}

final class StoragePool {
    private static $greyhole_owned_drives = array();
    private static $gone_ok_drives = NULL;
    private static $fscked_gone_drives = NULL;
    private static $last_df_time = 0;
    private static $last_dfs = [];
    public static function is_pool_drive($sp_drive) {
        global $going_drive;
        if (isset($going_drive) && $sp_drive == $going_drive) {
            return FALSE;
        }
        $is_greyhole_owned_drive = isset(self::$greyhole_owned_drives[$sp_drive]);
        if ($is_greyhole_owned_drive && self::$greyhole_owned_drives[$sp_drive] < time() - Config::get(CONFIG_DF_CACHE_TIME)) {
            unset(self::$greyhole_owned_drives[$sp_drive]);
            $is_greyhole_owned_drive = FALSE;
        }
        if (!$is_greyhole_owned_drive) {
            $drive_uuid = SystemHelper::directory_uuid($sp_drive);
            if (DB::isConnected()) {
                $drives_definitions = Settings::get('sp_drives_definitions', TRUE);
                if (!$drives_definitions) {
                    $drives_definitions = MigrationHelper::convertStoragePoolDrivesTagFiles();
                }
                $is_greyhole_owned_drive = @$drives_definitions[$sp_drive] === $drive_uuid && $drive_uuid !== FALSE;
            } else {
                $is_greyhole_owned_drive = is_dir("$sp_drive/.gh_metastore");
            }
            if (!$is_greyhole_owned_drive) {
                $is_greyhole_owned_drive = file_exists("$sp_drive/.greyhole_uses_this");
                if ($is_greyhole_owned_drive && isset($drives_definitions[$sp_drive])) {
                    unset($drives_definitions[$sp_drive]);
                    if (DB::isConnected()) {
                        Settings::set('sp_drives_definitions', $drives_definitions);
                    }
                }
            }
            if ($is_greyhole_owned_drive) {
                self::$greyhole_owned_drives[$sp_drive] = time();
            }
        }
        return $is_greyhole_owned_drive;
    }
    public static function check_drives() {
        Log::setAction(ACTION_CHECK_POOL);
        global $last_df_time;
        $force_run = ( time()-$last_df_time < 10);
        $schedule = Config::get(CONFIG_CHECK_SP_SCHEDULE);
        if (!empty($schedule) && !$force_run) {
            if (string_starts_with($schedule, '*:')) {
                if (strlen($schedule) == 4) {
                    $should_run = substr($schedule, 2) === date('i');
                } else {
                    Log::warn("Invalid format for " . CONFIG_CHECK_SP_SCHEDULE . " config option. Supported values are: *:mi or hh:mi", Log::EVENT_CODE_CONFIG_UNPARSEABLE_LINE);
                    $should_run = TRUE;
                }
            } else {
                if (strlen($schedule) == 5 && $schedule[2] == ':') {
                    $should_run = ( $schedule === date('H:i') );
                } else {
                    Log::warn("Invalid format for " . CONFIG_CHECK_SP_SCHEDULE . " config option. Supported values are: *:mi or hh:mi", Log::EVENT_CODE_CONFIG_UNPARSEABLE_LINE);
                    $should_run = TRUE;
                }
            }
            if (!$should_run) {
                return;
            }
        }
        $needs_fsck = FALSE;
        $drives_definitions = Settings::get('sp_drives_definitions', TRUE);
        $returned_drives = array();
        $missing_drives = array();
        $i = 0; $j = 0;
        foreach (Config::storagePoolDrives() as $sp_drive) {
            if (!self::is_pool_drive($sp_drive) && !self::gone_fscked($sp_drive, $i++ == 0) && !file_exists("$sp_drive/.greyhole_used_this") && !empty($drives_definitions[$sp_drive])) {
                if($needs_fsck !== 2){
                    $needs_fsck = 1;
                }
                self::mark_gone_drive_fscked($sp_drive);
                $missing_drives[] = $sp_drive;
                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 --replaced'. Because of that, Greyhole will NOT use this drive at this time.", Log::EVENT_CODE_STORAGE_POOL_DRIVE_UUID_CHANGED);
                Log::debug("Email sent for gone drive: $sp_drive");
                self::$gone_ok_drives[$sp_drive] = TRUE; // The upcoming fsck should not recreate missing copies just yet
            } else if ((self::gone_ok($sp_drive, $j++ == 0) || self::gone_fscked($sp_drive, $i++ == 0)) && self::is_pool_drive($sp_drive) && !empty($drives_definitions[$sp_drive])) {
                $needs_fsck = 2;
                $returned_drives[] = $sp_drive;
                Log::debug("Email sent for revived drive: $sp_drive");
                self::mark_gone_ok($sp_drive, 'remove');
                self::mark_gone_drive_fscked($sp_drive, 'remove');
                $i = 0; $j = 0;
            }
        }
        if (!empty($returned_drives)) {
            $body = "This is an automated email from Greyhole.\n\nOne (or more) of your storage pool drives came back:\n";
            foreach ($returned_drives as $sp_drive) {
                $body .= "$sp_drive was missing; it's now available again.\n";
            }
            $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";
            $drive_string = join(", ", $returned_drives);
            $subject = "Storage pool drive now online on " . exec ('hostname') . ": ";
            $subject = $subject . $drive_string;
            if (strlen($subject) > 255) {
                $subject = substr($subject, 0, 255);
            }
            email_sysadmin($subject, $body);
        }
        if (!empty($missing_drives)) {
            $body = "This is an automated email from Greyhole.\n\nOne (or more) of your storage pool drives has disappeared:\n";
            foreach ($missing_drives as $sp_drive) {
                if (!is_dir($sp_drive)) {
                    $body .= "$sp_drive: directory doesn't exists\n";
                } else {
                    $current_uuid = SystemHelper::directory_uuid($sp_drive);
                    if (empty($current_uuid)) {
                        $current_uuid = 'N/A';
                    }
                    $body .= "$sp_drive: expected partition UUID: " . $drives_definitions[$sp_drive] . "; current partition UUID: $current_uuid\n";
                }
            }
            $sp_drive = $missing_drives[0];
            $body .= "\nThis either means this mount is currently unmounted, or you forgot to use 'greyhole --replaced' when you changed this drive.\n\n";
            $body .= "Here are your options:\n\n";
            $body .= "- If you forgot to use 'greyhole --replaced', you should do so now. Until you do, this drive will not be part of your storage pool.\n\n";
            $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 --remove=" . 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";
            $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";
            $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";
            $subject = "Missing storage pool drives on " . exec('hostname') . ": ";
            $drive_string = join(",",$missing_drives);
            $subject = $subject . $drive_string;
            if (strlen($subject) > 255) {
                $subject = substr($subject, 0, 255);
            }
            email_sysadmin($subject, $body);
        }
        if ($needs_fsck !== FALSE) {
            Metastores::choose_metastores_backups();
            Metastores::get_metastores(FALSE); // FALSE => Resets the metastores cache
            clearstatcache();
            $fsck_task = FsckTask::getCurrentTask();
            $fsck_task->initialize_fsck_report('All shares');
            if ($needs_fsck === 2) {
                foreach ($returned_drives as $drive) {
                    $metastores = Metastores::get_metastores_from_storage_volume($drive);
                    Log::info("Starting fsck for metadata store on $drive which came back online.");
                    foreach ($metastores as $metastore) {
                        foreach (SharesConfig::getShares() as $share_name => $share_options) {
                            $fsck_task->gh_fsck_metastore($metastore,"/$share_name", $share_name);
                        }
                    }
                    Log::info("fsck for returning drive $drive's metadata store completed.");
                }
                Log::info("Starting fsck for all shares - caused by missing drive that came back online.");
            } else {
                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.");
                fix_all_symlinks();
            }
            schedule_fsck_all_shares(array('email'));
            Log::info("  fsck for all shares scheduled.");
            self::reload_gone_ok_drives();
        }
    }
    public static function gone_ok($sp_drive, $force_reload=FALSE) {
        if ($force_reload || self::$gone_ok_drives === NULL) {
            self::reload_gone_ok_drives();
        }
        if (isset(self::$gone_ok_drives[$sp_drive])) {
            return TRUE;
        }
        return FALSE;
    }
    public static function reload_gone_ok_drives() {
        self::$gone_ok_drives = Settings::get('Gone-OK-Drives', TRUE);
        if (self::$gone_ok_drives === FALSE) {
            self::$gone_ok_drives = array();
            Settings::set('Gone-OK-Drives', self::$gone_ok_drives);
        }
    }
    public static function get_gone_ok_drives() {
        if (self::$gone_ok_drives === NULL) {
            self::reload_gone_ok_drives();
        }
        return self::$gone_ok_drives;
    }
    public static function mark_gone_ok($sp_drive, $action='add') {
        if (!array_contains(Config::storagePoolDrives(), $sp_drive)) {
            $sp_drive = '/' . trim($sp_drive, '/');
        }
        if (!array_contains(Config::storagePoolDrives(), $sp_drive)) {
            return FALSE;
        }
        self::reload_gone_ok_drives();
        if ($action == 'add') {
            self::$gone_ok_drives[$sp_drive] = TRUE;
        } else {
            unset(self::$gone_ok_drives[$sp_drive]);
        }
        Settings::set('Gone-OK-Drives', self::$gone_ok_drives);
        return TRUE;
    }
    public static function gone_fscked($sp_drive, $force_reload=FALSE) {
        if ($force_reload || self::$fscked_gone_drives === NULL) {
            self::reload_fsck_gone_drives();
        }
        if (isset(self::$fscked_gone_drives[$sp_drive])) {
            return TRUE;
        }
        return FALSE;
    }
    public static function reload_fsck_gone_drives() {
        self::$fscked_gone_drives = Settings::get('Gone-FSCKed-Drives', TRUE);
        if (self::$fscked_gone_drives === FALSE) {
            self::$fscked_gone_drives = array();
            Settings::set('Gone-FSCKed-Drives', self::$fscked_gone_drives);
        }
    }
    public static function mark_gone_drive_fscked($sp_drive, $action='add') {
        self::reload_fsck_gone_drives();
        if ($action == 'add') {
            self::$fscked_gone_drives[$sp_drive] = TRUE;
        } else {
            unset(self::$fscked_gone_drives[$sp_drive]);
        }
        Settings::set('Gone-FSCKed-Drives', self::$fscked_gone_drives);
    }
    public static function remove_drive($going_drive) {
        $drives_definitions = Settings::get('sp_drives_definitions', TRUE);
        if (!$drives_definitions) {
            $drives_definitions = MigrationHelper::convertStoragePoolDrivesTagFiles();
        }
        unset($drives_definitions[$going_drive]);
        Settings::set('sp_drives_definitions', $drives_definitions);
    }
    public static function get_file_copies_inodes($share, $file_path, $filename, &$file_metafiles, $one_is_enough = FALSE) {
        $file_copies_inodes = [];
        foreach (Config::storagePoolDrives() as $sp_drive) {
            $clean_full_path = clean_dir("$sp_drive/$share/$file_path/$filename");
            if (is_link($clean_full_path)) {
                continue;
            }
            $inode_number = @gh_fileinode($clean_full_path);
            if ($inode_number !== FALSE) {
                if (is_dir($clean_full_path)) {
                    Log::info("Found a directory that should be a file! Will try to remove it, if it's empty.");
                    @rmdir($clean_full_path);
                    continue;
                }
                Log::debug("Found $clean_full_path");
                if (!StoragePool::is_pool_drive($sp_drive)) {
                    $state = Metafile::STATE_GONE;
                    if (!$one_is_enough) {
                        Log::info("  Drive $sp_drive is not part of the Greyhole storage pool anymore. The above file will not be counted as a valid file copy, but can be used to create a new valid copy.");
                    }
                } else {
                    $state = Metafile::STATE_OK;
                    $file_copies_inodes[$inode_number] = $clean_full_path;
                    if ($one_is_enough) {
                        return $file_copies_inodes;
                    }
                }
                if (is_string($file_metafiles)) {
                    Log::critical("Fatal error! \$file_metafiles is now a string: '$file_metafiles'.", Log::EVENT_CODE_UNEXPECTED_VAR);
                }
                $file_metafiles[$clean_full_path] = (object) array('path' => $clean_full_path, 'is_linked' => FALSE, 'state' => $state);
                $temp_filename = StorageFile::get_temp_filename($clean_full_path);
                if (file_exists($temp_filename) && gh_is_file($temp_filename)) {
                    Log::info("  Found temporary file $temp_filename ... deleting.");
                    $fsck_report['temp_files'][] = $temp_filename;
                    unlink($temp_filename);
                }
            }
        }
        return $file_copies_inodes;
    }
    public static function get_free_space($for_sp_drive) {
        if (time() > StoragePool::$last_df_time + Config::get(CONFIG_DF_CACHE_TIME)) {
            $dfs = [];
            exec(ConfigHelper::$df_command, $responses);
            $responses_arr = array();
            foreach ($responses as $line) {
                if (preg_match("@\s+[0-9]+\s+([0-9]+)\s+([0-9]+)\s+[0-9]+%\s+(.+)$@", $line, $regs)) {
                    $responses_arr[] = array((float) $regs[1], (float) $regs[2], $regs[3]);
                }
            }
            $responses = $responses_arr;
            foreach (Config::storagePoolDrives() as $sp_drive) {
                if (!StoragePool::is_pool_drive($sp_drive)) {
                    continue;
                }
                $target_drive = '';
                unset($target_freespace);
                unset($target_usedspace);
                for ($i=0; $i<count($responses); $i++) {
                    $used_space = $responses[$i][0];
                    $free_space = $responses[$i][1];
                    $mount = $responses[$i][2];
                    if (mb_strpos($sp_drive, $mount) === 0 && mb_strlen($mount) > mb_strlen($target_drive)) {
                        $target_drive = $mount;
                        $target_freespace = $free_space;
                        $target_usedspace = $used_space;
                    }
                }
                if (empty($target_drive)) {
                    unset($responses);
                    exec('df -k ' . $sp_drive, $responses);
                    foreach ($responses as $line) {
                        if (preg_match("@\s+[0-9]+\s+([0-9]+)\s+([0-9]+)\s+[0-9]+%\s+(.+)$@", $line, $regs)) {
                            $target_freespace = (float) $regs[2];
                            $target_usedspace = (float) $regs[1];
                        }
                    }
                }
                $dfs[$sp_drive]['free'] = $target_freespace;
                $dfs[$sp_drive]['used'] = $target_usedspace;
            }
            StoragePool::$last_df_time = time();
            StoragePool::$last_dfs = $dfs;
        }
        if (empty(StoragePool::$last_dfs[$for_sp_drive])) {
            return FALSE;
        }
        return StoragePool::$last_dfs[$for_sp_drive];
    }
    public static function choose_target_drives($filesize_kb, $include_full_drives, $share, $path, $log_prefix = '', &$is_sticky = NULL) {
        global $last_OOS_notification;
        foreach (SharesConfig::get($share, CONFIG_DRIVE_SELECTION_ALGORITHM) as $ds) {
            $algo = $ds->selection_algorithm;
            break;
        }
        $sorted_target_drives = array('available_space' => array(), 'used_space' => array());
        $last_resort_sorted_target_drives = array('available_space' => array(), 'used_space' => array());
        $full_drives = array();
        foreach (Config::storagePoolDrives() as $sp_drive) {
            $df = StoragePool::get_free_space($sp_drive);
            if (!$df) {
                if (!is_dir($sp_drive)) {
                    if (SystemHelper::is_amahi()) {
                        $details = "You should de-select, then re-select this partition in your Amahi dashboard (http://hda), in the Shares > Storage Pool page, to fix this problem.";
                    } else {
                        $details = "See the INSTALL file for instructions on how to prepare partitions to include in your storage pool.";
                    }
                    Log::error("The directory at $sp_drive doesn't exist. This drive will never be used! $details", Log::EVENT_CODE_STORAGE_POOL_FOLDER_NOT_FOUND);
                } else if (!file_exists("$sp_drive/.greyhole_used_this") && StoragePool::is_pool_drive($sp_drive)) {
                    unset($df_command_responses);
                    exec(ConfigHelper::$df_command, $df_command_responses);
                    unset($df_k_responses);
                    exec('df -k 2>&1', $df_k_responses);
                    $details = "Please report this using the 'Issues' tab found on https://github.com/gboudreau/Greyhole. You should include the following information in your ticket:\n"
                        . "===== Error report starts here =====\n"
                        . "Unknown free space for partition: $sp_drive\n"
                        . "df_command: " . ConfigHelper::$df_command . "\n"
                        . "Result of df_command: " . var_export($df_command_responses, TRUE) . "\n"
                        . "Result of df -k: " . var_export($df_k_responses, TRUE) . "\n"
                        . "===== Error report ends here =====";
                    Log::error("Can't find how much free space is left on $sp_drive. This partition will never be used! Details will follow.\n$details", Log::EVENT_CODE_STORAGE_POOL_DRIVE_DF_FAILED);
                }
                continue;
            }
            if (!StoragePool::is_pool_drive($sp_drive)) {
                continue;
            }
            $free_space = $df['free'];
            $used_space = $df['used'];
            $minimum_free_space = (float) Config::get(CONFIG_MIN_FREE_SPACE_POOL_DRIVE, $sp_drive);
            $available_space = (float) $free_space - $minimum_free_space;
            if ($available_space <= $filesize_kb) {
                if ($free_space > $filesize_kb) {
                    $last_resort_sorted_target_drives['available_space'][$sp_drive] = $available_space;
                    $last_resort_sorted_target_drives['used_space'][$sp_drive] = $used_space;
                } else {
                    $full_drives[$sp_drive] = $free_space;
                }
                continue;
            }
            $sorted_target_drives['available_space'][$sp_drive] = $available_space;
            $sorted_target_drives['used_space'][$sp_drive] = $used_space;
        }
        $drives_selectors = SharesConfig::get($share, CONFIG_DRIVE_SELECTION_ALGORITHM);
        foreach ($drives_selectors as $ds) {
            $s = $sorted_target_drives;
            $l = $last_resort_sorted_target_drives;
            $ds->init($s, $l);
        }
        $sorted_target_drives = array();
        $last_resort_sorted_target_drives = array();
        while (TRUE) {
            $num_empty_ds = 0;
            global $is_forced;
            foreach ($drives_selectors as $ds) {
                $is_forced = $ds->isForced();
                list($drives, $drives_last_resort) = $ds->draft();
                foreach ($drives as $sp_drive => $space) {
                    $sorted_target_drives[$sp_drive] = $space;
                }
                foreach ($drives_last_resort as $sp_drive => $space) {
                    $last_resort_sorted_target_drives[$sp_drive] = $space;
                }
                if (empty($drives) && count($drives_last_resort) == 0) {
                    $num_empty_ds++;
                }
            }
            if ($num_empty_ds == count($drives_selectors)) {
                break;
            }
        }
        if (empty($sorted_target_drives)) {
            Log::error("  Warning! All storage pool drives are over-capacity!", Log::EVENT_CODE_ALL_DRIVES_FULL);
            if (!isset($last_OOS_notification)) {
                $setting = Settings::get('last_OOS_notification');
                if ($setting === FALSE) {
                    Log::warn("Received no rows when querying settings for 'last_OOS_notification'; expected one.", Log::EVENT_CODE_SETTINGS_READ_ERROR);
                    $setting = Settings::set('last_OOS_notification', 0);
                }
                $last_OOS_notification = $setting;
            }
            if ($last_OOS_notification < strtotime('-1 day')) {
                $hostname = exec('hostname');
                $body = "This is an automated email from Greyhole.
It appears all the defined storage pool drives are over-capacity.
You probably want to do something about this!
";
                foreach ($last_resort_sorted_target_drives as $sp_drive => $free_space) {
                    $minimum_free_space = (int) Config::get(CONFIG_MIN_FREE_SPACE_POOL_DRIVE, $sp_drive) / 1024 / 1024;
                    $body .= "$sp_drive has " . number_format($free_space/1024/1024, 2) . " GB free; minimum specified in greyhole.conf: $minimum_free_space GB.\n";
                }
                email_sysadmin("Greyhole is out of space on $hostname!", $body);
                $last_OOS_notification = time();
                Settings::set('last_OOS_notification', $last_OOS_notification);
            }
        }
        if (Log::getLevel() >= Log::DEBUG) {
            if (!empty($sorted_target_drives)) {
                $log = $log_prefix . "Drives with available space: ";
                foreach ($sorted_target_drives as $sp_drive => $space) {
                    $log .= "$sp_drive (" . bytes_to_human($space*1024, FALSE) . " " . ($algo == 'most_available_space' ? 'avail' : 'used') . ") - ";
                }
                Log::debug(mb_substr($log, 0, mb_strlen($log)-2));
            }
            if (!empty($last_resort_sorted_target_drives)) {
                $log = $log_prefix . "Drives with enough free space, but no available space: ";
                foreach ($last_resort_sorted_target_drives as $sp_drive => $space) {
                    $log .= "$sp_drive (" . bytes_to_human($space*1024, FALSE) . " " . ($algo == 'most_available_space' ? 'avail' : 'used') . ") - ";
                }
                Log::debug(mb_substr($log, 0, mb_strlen($log)-2));
            }
            if (!empty($full_drives)) {
                $log = $log_prefix . "Drives full: ";
                foreach ($full_drives as $sp_drive => $free_space) {
                    $log .= "$sp_drive (" . bytes_to_human($free_space*1024, FALSE) . " free) - ";
                }
                Log::debug(mb_substr($log, 0, mb_strlen($log)-2));
            }
        }
        $sorted_target_drives = array_keys($sorted_target_drives);
        $last_resort_sorted_target_drives = array_keys($last_resort_sorted_target_drives);
        $full_drives = array_keys($full_drives);
        $drives = array_merge($sorted_target_drives, $last_resort_sorted_target_drives);
        if ($include_full_drives) {
            $drives = array_merge($drives, $full_drives);
        }
        $sticky_files = Config::get(CONFIG_STICKY_FILES);
        if (!empty($sticky_files)) {
            $is_sticky = FALSE;
            foreach ($sticky_files as $share_dir => $stick_into) {
                if (gh_wild_mb_strpos("$share/$path", $share_dir) === 0) {
                    $is_sticky = TRUE;
                    $more_drives_needed = FALSE;
                    if (!empty($stick_into)) {
                        foreach ($stick_into as $key => $stick_into_dir) {
                            if (!array_contains(Config::storagePoolDrives(), $stick_into_dir)) {
                                unset($stick_into[$key]);
                                $more_drives_needed = TRUE;
                            }
                        }
                    }
                    if (empty($stick_into) || $more_drives_needed) {
                        if (string_contains($share_dir, '*')) {
                            $needles = explode('*', $share_dir);
                            $sticky_dir = '';
                            $wild_part = "$share/$path/";
                            for ($i=0; $i<count($needles); $i++) {
                                $needle = $needles[$i];
                                if ($i == 0) {
                                    $sticky_dir = $needle;
                                    $wild_part = @str_replace_first($needle, '', $wild_part);
                                } else {
                                    if ($needle == '') {
                                        $needle = '/';
                                    }
                                    $small_wild_part = mb_substr($wild_part, 0, mb_strpos($wild_part, $needle)+mb_strlen($needle));
                                    $sticky_dir .= $small_wild_part;
                                    $wild_part = str_replace_first($small_wild_part, '', $wild_part);
                                }
                            }
                            $sticky_dir = trim($sticky_dir, '/');
                        } else {
                            $sticky_dir = $share_dir;
                        }
                        $setting_name = sprintf('stick_into-%s', $sticky_dir);
                        $setting = Settings::get($setting_name, TRUE);
                        if ($setting) {
                            $stick_into = array_merge($stick_into, $setting);
                            $update_needed = FALSE;
                            foreach ($stick_into as $key => $stick_into_dir) {
                                if (!array_contains(Config::storagePoolDrives(), $stick_into_dir)) {
                                    unset($stick_into[$key]);
                                    $update_needed = TRUE;
                                }
                            }
                            if ($update_needed) {
                                $value = serialize($stick_into);
                                Settings::set($setting_name, $value);
                            }
                        } else {
                            $value = array_merge($stick_into, $drives);
                            Settings::set($setting_name, $value);
                        }
                    }
                    $priority_drives = array();
                    foreach ($stick_into as $stick_into_dir) {
                        if (array_contains(Config::storagePoolDrives(), $stick_into_dir)
                            && !array_contains($full_drives, $stick_into_dir)
                            && !array_contains($last_resort_sorted_target_drives, $stick_into_dir)) {
                            $priority_drives[] = $stick_into_dir;
                            unset($drives[array_search($stick_into_dir, $drives)]);
                        }
                    }
                    $drives = array_merge($priority_drives, $drives);
                    Log::debug($log_prefix . "Reordered drives, per sticky_files config: " . implode(' - ', $drives));
                    break;
                }
            }
        }
        return $drives;
    }
    public static function get_drives_available_space() {
        $sorted_target_drives = [];
        foreach (Config::storagePoolDrives() as $sp_drive) {
            $df = StoragePool::get_free_space($sp_drive);
            if (!$df) {
                continue;
            }
            $free_space = $df['free'];
            $minimum_free_space = Config::get(CONFIG_MIN_FREE_SPACE_POOL_DRIVE, $sp_drive);
            $available_space = (float) $free_space - $minimum_free_space;
            $sorted_target_drives[$sp_drive] = $available_space;
        }
        asort($sorted_target_drives);
        return $sorted_target_drives;
    }
    public static function getDriveFromPath($full_path) {
        $storage_volume = '';
        foreach (Config::storagePoolDrives() as $sp_drive) {
            if (string_starts_with($full_path, $sp_drive) && mb_strlen($sp_drive) > mb_strlen($storage_volume)) {
                $storage_volume = $sp_drive;
            }
        }
        return empty($storage_volume) ? FALSE : $storage_volume;
    }
}

final class SystemHelper {
    public static function is_amahi() {
        return file_exists('/usr/bin/hda-ctl');
    }
    public static function directory_uuid($dir) {
        $dev = exec('df ' . escapeshellarg($dir) . ' 2> /dev/null | grep \'/dev\' | awk \'{print $1}\'');
        if (!is_dir($dir)) {
            return FALSE;
        }
        if (empty($dev) || strpos($dev, '/dev/') !== 0) {
            if (file_exists('/sbin/zpool')) {
                $dataset = exec('df ' . escapeshellarg($dir) . ' 2> /dev/null | awk \'{print $1}\'');
                if (strpos($dataset, '/') !== FALSE) {
                    $is_zfs = exec('mount | grep ' . escapeshellarg("$dataset .*zfs") . ' 2> /dev/null | wc -l');
                    if ($is_zfs == 1) {
                        $p = explode('/', $dataset);
                        $pool = $p[0];
                        $dev_name = exec('/sbin/zpool list -v ' . escapeshellarg($pool) . ' 2> /dev/null | awk \'{print $1}\' | tail -n 1');
                        if (!empty($dev_name)) {
                            $dev = exec("ls -l /dev/disk/*/$dev_name | awk '{print \$(NF-2)}'");
                            if (empty($dev) && file_exists("/dev/$dev_name")) {
                                $dev = '/dev/$dev_name';
                                Log::info("Found a ZFS pool ($pool) that uses a device name in /dev/ ($dev). That is a bad idea, since those can easily change, which would prevent this pool from mounting automatically. You should use any of the /dev/disk/*/ links instead. For example, you could do: zpool export $pool && zpool import -d /dev/disk/by-id/ $pool. More details at http://zfsonlinux.org/faq.html#WhatDevNamesShouldIUseWhenCreatingMyPool");
                            }
                        }
                        if (empty($dev)) {
                            Log::warn("Warning! Couldn't find the device used by your ZFS pool name $pool. That pool will never be used.", Log::EVENT_CODE_ZFS_UNKNOWN_DEVICE);
                            return FALSE;
                        }
                    }
                }
            }
            if (empty($dev)) {
                return 'remote';
            }
        }
        $uuid = trim(exec('/sbin/blkid '.$dev.' | awk -F\'UUID="\' \'{print $2}\' | awk -F\'"\' \'{print $1}\''));
        if (empty($uuid)) {
            return 'remote';
        }
        return $uuid;
    }
}
$use_alt_symlinks_creation = FALSE;
function gh_symlink($target, $link) {
    global $use_alt_symlinks_creation;
    $success = !$use_alt_symlinks_creation && symlink($target, $link);
    if (!$success) {
        exec("ln -s " . escapeshellarg($target) . " " . escapeshellarg($link));
        $success = gh_is_file($link);
        if ($success) {
            if (!$use_alt_symlinks_creation) {
                Log::info("Will use exec() instead of symlink() to create all symlinks.");
            }
            $use_alt_symlinks_creation = TRUE;
        }
    }
    return $success;
}
function gh_mkdir($directory, $original_directory, $dir_permissions = NULL) {
    $directory = clean_dir($directory);
    $original_directory = clean_dir($original_directory);
    if (is_dir($directory)) {
        if (empty($dir_permissions)) {
            $dir_permissions = StorageFile::get_file_permissions($original_directory);
        } else {
        }
        if (!chown($directory, $dir_permissions->fileowner)) {
            Log::warn("  Failed to chown directory '$directory'", Log::EVENT_CODE_MKDIR_CHOWN_FAILED);
        }
        if (!chgrp($directory, $dir_permissions->filegroup)) {
            Log::warn("  Failed to chgrp directory '$directory'", Log::EVENT_CODE_MKDIR_CHGRP_FAILED);
        }
        if (!chmod($directory, $dir_permissions->fileperms)) {
            Log::warn("  Failed to chmod directory '$directory'", Log::EVENT_CODE_MKDIR_CHMOD_FAILED);
        }
    } else {
        $dir_parts = explode('/', $directory);
        $dir_parts_orig = explode('/', $original_directory);
        $i = 0;
        $parent_directory = clean_dir('/' . $dir_parts[$i++]);
        while (is_dir($parent_directory) && $i < count($dir_parts)) {
            $parent_directory = clean_dir($parent_directory . '/' . $dir_parts[$i++]);
        }
        while ($i <= count($dir_parts)) {
            if (!empty($dir_permissions)) {
                $new_dir_permissions = $dir_permissions;
            } else {
                $new_original_directory = '';
                for ($j=0; $j<count($dir_parts_orig) - (count($dir_parts) - $i); $j++) {
                    $new_original_directory = clean_dir($new_original_directory . '/' . $dir_parts_orig[$j]);
                }
                $new_dir_permissions = StorageFile::get_file_permissions($new_original_directory);
            }
            if (!is_dir($parent_directory) && !@mkdir($parent_directory, $new_dir_permissions->fileperms)) {
                if (gh_is_file($parent_directory)) {
                    gh_rename($parent_directory, "$parent_directory (file copy)");
                }
                if (!@mkdir($parent_directory, $new_dir_permissions->fileperms)) {
                    if (!is_dir($parent_directory)) {
                        if (is_dir(normalize_utf8_characters($parent_directory))) {
                            $parent_directory = normalize_utf8_characters($parent_directory);
                        } else {
                            Log::warn("  Failed to create directory $parent_directory", Log::EVENT_CODE_MKDIR_FAILED);
                            return FALSE;
                        }
                    }
                }
            }
            if (!chown($parent_directory, $new_dir_permissions->fileowner)) {
                Log::warn("  Failed to chown directory '$parent_directory'", Log::EVENT_CODE_MKDIR_CHOWN_FAILED);
            }
            if (!chgrp($parent_directory, $new_dir_permissions->filegroup)) {
                Log::warn("  Failed to chgrp directory '$parent_directory'", Log::EVENT_CODE_MKDIR_CHGRP_FAILED);
            }
            if (!isset($dir_parts[$i])) {
                break;
            }
            $parent_directory = clean_dir($parent_directory . '/' . $dir_parts[$i++]);
        }
    }
    return TRUE;
}
function gh_file_exists($real_path, $log_message = NULL) {
    clearstatcache();
    if (!file_exists($real_path)) {
        if (!empty($log_message)) {
            eval('$log_message = "' . str_replace('"', '\"', $log_message) . '";');
            Log::info($log_message);
        }
        return FALSE;
    }
    return TRUE;
}
function gh_is_file_locked($real_fullpath) {
    if (is_link($real_fullpath)) {
        $real_fullpath = readlink($real_fullpath);
    }
    $result = exec("lsof -n -P -l " . escapeshellarg($real_fullpath) . " 2> /dev/null");
    if (string_contains($result, $real_fullpath)) {
        if (preg_match('/^([^\s]+)\s+(\d+)\s+(\d+)\s+([^\s]+)\s/U', $result, $re)) {
            if (substr($re[4], -1) == 'r') {
                return FALSE;
            }
            return "$re[1], PID $re[2]";
        }
        return $result;
    }
    return FALSE;
}
$arch = exec('uname -m');
if ($arch != 'x86_64') {
    function gh_filesize($filename) {
        $result = exec("stat -c %s ".escapeshellarg($filename)." 2>/dev/null");
        if (empty($result)) {
            return FALSE;
        }
        return (float) $result;
    }
    function gh_fileowner($filename) {
        $result = exec("stat -c %u ".escapeshellarg($filename)." 2>/dev/null");
        if (empty($result)) {
            return FALSE;
        }
        return (int) $result;
    }
    function gh_filegroup($filename) {
        $result = exec("stat -c %g ".escapeshellarg($filename)." 2>/dev/null");
        if (empty($result)) {
            return FALSE;
        }
        return (int) $result;
    }
    function gh_fileperms($filename) {
        $result = exec("stat -c %a ".escapeshellarg($filename)." 2>/dev/null");
        if (empty($result)) {
            return FALSE;
        }
        return "0" . $result;
    }
    function gh_is_file($filename) {
        exec('[ -f '.escapeshellarg($filename).' ]', $tmp, $result);
        return $result === 0;
    }
    function gh_fileinode($filename) {
        $result = exec("stat -c '%d_%i' ".escapeshellarg($filename)." 2>/dev/null");
        if (empty($result)) {
            return FALSE;
        }
        return (string) $result;
    }
    function gh_file_deviceid($filename) {
        $result = exec("stat -c '%d' ".escapeshellarg($filename)." 2>/dev/null");
        if (empty($result)) {
            return FALSE;
        }
        return (string) $result;
    }
    function gh_rename($filename, $target_filename) {
        exec("mv ".escapeshellarg($filename)." ".escapeshellarg($target_filename)." 2>/dev/null", $output, $result);
        return $result === 0;
    }
} else {
    function gh_filesize(&$filename) {
        $size = @filesize($filename);
        if ($size === FALSE) {
            $size = @filesize(normalize_utf8_characters($filename));
            if ($size !== FALSE) {
                $filename = normalize_utf8_characters($filename);
            }
        }
        return $size;
    }
    function gh_fileowner($filename) {
        return fileowner($filename);
    }
    function gh_filegroup($filename) {
        return filegroup($filename);
    }
    function gh_fileperms($filename) {
        return mb_substr(decoct(fileperms($filename)), -4);
    }
    function gh_is_file($filename) {
        return is_file($filename);
    }
    function gh_fileinode($filename) {
        $stat = @stat($filename);
        if ($stat === FALSE) {
            return FALSE;
        }
        return $stat['dev'] . '_' . $stat['ino'];
    }
    function gh_file_deviceid($filename) {
        $stat = @stat($filename);
        if ($stat === FALSE) {
            return FALSE;
        }
        return $stat['dev'];
    }
    function gh_rename($filename, $target_filename) {
        return @rename($filename, $target_filename);
    }
}

final class Trash {
    public static function trash_file($real_path, $file_was_modified = FALSE) {
        $is_symlink = FALSE;
        clearstatcache();
        if (is_link($real_path)) {
            $is_symlink = TRUE;
        } else if (!file_exists($real_path)) {
            return TRUE;
        }
        $should_move_to_trash = FALSE;
        if (!$is_symlink) {
            $share_options = SharesConfig::getShareOptions($real_path);
            if ($share_options !== FALSE) {
                $full_path = trim($share_options['name'] . "/" . str_replace($share_options[CONFIG_LANDING_ZONE], '', $real_path), '/');
            } else {
                $storage_volume = StoragePool::getDriveFromPath($real_path);
                foreach (Config::storagePoolDrives() as $sp_drive) {
                    if ($sp_drive == $storage_volume) {
                        $trash_path = "$sp_drive/.gh_trash";
                        $full_path = trim(substr($real_path, strlen($sp_drive)), '/');
                        break;
                    }
                }
                $share = mb_substr($full_path, 0, mb_strpos($full_path, '/'));
                if ($file_was_modified) {
                    $should_move_to_trash = SharesConfig::get($share, CONFIG_MODIFIED_MOVES_TO_TRASH);
                } else {
                    $should_move_to_trash = SharesConfig::get($share, CONFIG_DELETE_MOVES_TO_TRASH);
                }
            }
        }
        if ($should_move_to_trash) {
            if (!isset($trash_path)) {
                Log::warn("  Warning! Can't find trash for $real_path. Won't delete this file!", Log::EVENT_CODE_TRASH_NOT_FOUND);
                return FALSE;
            }
            $target_path = clean_dir("$trash_path/$full_path");
            list($path, ) = explode_full_path($target_path);
            if (@gh_is_file($path)) {
                unlink($path);
            }
            $dir_infos = (object) array(
                'fileowner' => 0,
                'filegroup' => 0,
                'fileperms' => (int) base_convert("0777", 8, 10)
            );
            gh_mkdir($path, NULL, $dir_infos);
            if (@is_dir($target_path)) {
                exec("rm -rf " . escapeshellarg($target_path));
            }
            if (@gh_rename($real_path, $target_path)) {
                Log::debug("  Moved copy from $real_path to trash: $target_path");
                static::create_trash_share_symlink($target_path, $trash_path);
                return TRUE;
            }
        } else {
            if (@unlink($real_path)) {
                if (!$is_symlink) {
                    Log::debug("  Deleted copy at $real_path");
                }
                return TRUE;
            }
        }
        return FALSE;
    }
    private static function create_trash_share_symlink($filepath_in_trash, $trash_path) {
        $trash_share = SharesConfig::getConfigForShare(CONFIG_TRASH_SHARE);
        if ($trash_share) {
            $filepath_in_trash = clean_dir($filepath_in_trash);
            $filepath_in_trash_share = str_replace($trash_path, $trash_share[CONFIG_LANDING_ZONE], $filepath_in_trash);
            if (file_exists($filepath_in_trash_share)) {
                $new_filepath = $filepath_in_trash_share;
                $i = 1;
                while (file_exists($new_filepath)) {
                    if (@readlink($new_filepath) == $filepath_in_trash) {
                        return;
                    }
                    $new_filepath = "$filepath_in_trash_share copy $i";
                    $i++;
                }
                $filepath_in_trash_share = $new_filepath;
                list(, $filename) = explode_full_path($filepath_in_trash_share);
            } else {
                list($original_path, ) = explode_full_path($filepath_in_trash);
                list($path, $filename) = explode_full_path($filepath_in_trash_share);
                $dir_infos = (object) array(
                    'fileowner' => (int) gh_fileowner($original_path),
                    'filegroup' => (int) gh_filegroup($original_path),
                    'fileperms' => (int) base_convert("0777", 8, 10)
                );
                gh_mkdir($path, NULL, $dir_infos);
            }
            if (@gh_symlink($filepath_in_trash, $filepath_in_trash_share)) {
                Log::debug("  Created symlink to deleted file in {$trash_share['name']} share ($filename).");
            } else {
                Log::warn("  Warning! Couldn't create symlink to deleted file in {$trash_share['name']} share ($filename).", Log::EVENT_CODE_TRASH_SYMLINK_FAILED);
            }
        }
    }
}

$constarray = get_defined_constants(true);
foreach($constarray['user'] as $key => $val) {
    eval(sprintf('$_CONSTANTS[\'%s\'] = ' . (is_int($val) || is_float($val) ? '%s' : "'%s'") . ';', addslashes($key), addslashes($val)));
}
define('FSCK_TYPE_SHARE', 1);
define('FSCK_TYPE_STORAGE_POOL_DRIVE', 2);
define('FSCK_TYPE_METASTORE', 3);
set_error_handler("gh_error_handler");
register_shutdown_function("gh_shutdown");
umask(0);
setlocale(LC_COLLATE, "en_US.UTF-8");
setlocale(LC_CTYPE, "en_US.UTF-8");
if (!defined('PHP_VERSION_ID')) {
    $version = explode('.', PHP_VERSION);
    define('PHP_VERSION_ID', (((int) $version[0] * 10000) + ((int) $version[1] * 100) + (int) $version[2]));
}
DBSpool::resetSleepingTasks();
function clean_dir($dir) {
    if (empty($dir)) {
        return $dir;
    }
    if ($dir[0] == '.' && $dir[1] == '/') {
        $dir = mb_substr($dir, 2);
    }
    while (string_contains($dir, '//')) {
        $dir = str_replace("//", "/", $dir);
    }
    $l = strlen($dir);
    if ($l >= 2 && $dir[$l-2] == '/' && $dir[$l-1] == '.') {
        $dir = mb_substr($dir, 0, $l-2);
    }
    $dir = str_replace("/./", "/", $dir);
    return $dir;
}
function explode_full_path($full_path) {
    return array(dirname($full_path), basename($full_path));
}
function gh_shutdown() {
    if ($err = error_get_last()) {
        Log::error("PHP Fatal Error: " . $err['message'] . "; BT: " . basename($err['file']) . '[L' . $err['line'] . '] ', Log::EVENT_CODE_PHP_CRITICAL);
    }
}
function gh_error_handler($errno, $errstr, $errfile, $errline, $errcontext = NULL) {
    if(!($errno & error_reporting())) {
        return TRUE;
    }
    switch ($errno) {
    case E_ERROR:
    case E_PARSE:
    case E_CORE_ERROR:
    case E_COMPILE_ERROR:
        Log::critical("PHP Error [$errno]: $errstr in $errfile on line $errline", Log::EVENT_CODE_PHP_CRITICAL);
        break;
    case E_WARNING:
    case E_COMPILE_WARNING:
    case E_CORE_WARNING:
    case E_NOTICE:
        $greyhole_log_file = Config::get(CONFIG_GREYHOLE_LOG_FILE);
        if ($errstr == "fopen($greyhole_log_file): failed to open stream: Permission denied") {
            return TRUE;
        }
        Log::warn("PHP Warning [$errno]: $errstr in $errfile on line $errline; BT: " . get_debug_bt(), Log::EVENT_CODE_PHP_WARNING);
        break;
    default:
        Log::error("PHP Unknown Error [$errno]: $errstr in $errfile on line $errline", Log::EVENT_CODE_PHP_ERROR);
        break;
    }
    return TRUE;
}
function get_debug_bt() {
    $bt = '';
    foreach (debug_backtrace() as $d) {
        if ($d['function'] == 'gh_error_handler' || $d['function'] == 'get_debug_bt') { continue; }
        if ($bt != '') {
            $bt = " => $bt";
        }
        $prefix = '';
        if (isset($d['file'])) {
            $prefix = basename($d['file']) . '[L' . $d['line'] . '] ';
        }
        if (!isset($d['args'])) {
            $d['args'] = [];
        }
        $args = [];
        foreach ($d['args'] as $v) {
            if (is_object($v)) {
                $args[] = 'stdClass';
            } elseif (is_array($v)) {
                $args[] = str_replace("\n", "", var_export($v, TRUE));
            } else {
                $args[] = $v;
            }
        }
        $bt = $prefix . $d['function'] .'(' . implode(',', $args) . ')' . $bt;
    }
    return $bt;
}
function bytes_to_human($bytes, $html=TRUE, $iso_units=FALSE) {
    $units = 'B';
    if (abs($bytes) > 1024) {
        $bytes /= 1024;
        $units = $iso_units ? 'KiB' : 'KB';
    }
    if (abs($bytes) > 1024) {
        $bytes /= 1024;
        $units = $iso_units ? 'MiB' : 'MB';
    }
    if (abs($bytes) > 1024) {
        $bytes /= 1024;
        $units = $iso_units ? 'GiB' : 'GB';
    }
    if (abs($bytes) > 1024) {
        $bytes /= 1024;
        $units = $iso_units ? 'TiB' : 'TB';
    }
    $decimals = (abs($bytes) > 100 ? 0 : (abs($bytes) > 10 ? 1 : 2));
    if ($html) {
        return number_format($bytes, $decimals) . " <span class=\"i18n-$units\">$units</span>";
    } else {
        return number_format($bytes, $decimals) . $units;
    }
}
function duration_to_human($seconds) {
    $displayable_duration = '';
    if ($seconds > 60*60) {
        $hours = floor($seconds / (60*60));
        $displayable_duration .= $hours . 'h ';
        $seconds -= $hours * (60*60);
    }
    if ($seconds > 60) {
        $minutes = floor($seconds / 60);
        $displayable_duration .= $minutes . 'm ';
        $seconds -= $minutes * 60;
    }
    $displayable_duration .= $seconds . 's';
    return $displayable_duration;
}
function get_share_landing_zone($share) {
    $lz = SharesConfig::get($share, CONFIG_LANDING_ZONE);
    if ($lz !== FALSE) {
        return $lz;
    } else if (array_contains(ConfigHelper::$trash_share_names, $share)) {
        return SharesConfig::get(CONFIG_TRASH_SHARE, CONFIG_LANDING_ZONE);
    } else {
        Log::warn("  Found a share ($share) with no path in " . ConfigHelper::$smb_config_file . ", or missing it's num_copies[$share] config in " . ConfigHelper::$config_file . ". Skipping.", Log::EVENT_CODE_SHARE_MISSING_FROM_GREYHOLE_CONF);
        return FALSE;
    }
}
function memory_check() {
    $usage = memory_get_usage();
    $used = $usage / Config::get(CONFIG_MEMORY_LIMIT);
    $used = $used * 100;
    if ($used > 95) {
        Log::critical("$used% memory usage, exiting. Please increase '" . CONFIG_MEMORY_LIMIT . "' in " . ConfigHelper::$config_file, Log::EVENT_CODE_MEMORY_LIMIT_REACHED);
    }
}
function email_sysadmin($subject, $body) {
    $email_to = Config::get(CONFIG_EMAIL_TO);
    Log::debug("Sending email to $email_to; subject: $subject");
    mail($email_to, $subject, $body);
    EmailHook::trigger($subject, $body);
}
define('FSCK_COUNT_META_FILES',           1);
define('FSCK_COUNT_META_DIRS',            2);
define('FSCK_COUNT_LZ_FILES',             3);
define('FSCK_COUNT_LZ_DIRS',              4);
define('FSCK_COUNT_ORPHANS',              5);
define('FSCK_COUNT_SYMLINK_TARGET_MOVED', 6);
define('FSCK_COUNT_TOO_MANY_COPIES',      7);
define('FSCK_COUNT_MISSING_COPIES',       8);
define('FSCK_COUNT_GONE_OK',              9);
define('FSCK_PROBLEM_NO_COPIES_FOUND',   10);
define('FSCK_PROBLEM_TOO_MANY_COPIES',   11);
define('FSCK_PROBLEM_WRONG_COPY_SIZE',   12);
define('FSCK_PROBLEM_TEMP_FILE',         13);
define('FSCK_PROBLEM_WRONG_MD5',         14);
class FSCKReport {
    public $start;
    public $end;
    public $what;
    public $send_via_email;
    public $counts;
    public $found_problems;
    protected static $dirs = [];
    public function __construct($what, $reset_dirs = FALSE) {
        $this->start          = time();
        $this->end            = NULL;
        $this->what           = $what;
        $this->send_via_email = FALSE;
        $this->counts = array();
        foreach (array(FSCK_COUNT_META_FILES, FSCK_COUNT_META_DIRS, FSCK_COUNT_LZ_FILES, FSCK_COUNT_LZ_DIRS, FSCK_COUNT_ORPHANS, FSCK_COUNT_SYMLINK_TARGET_MOVED, FSCK_COUNT_TOO_MANY_COPIES, FSCK_COUNT_MISSING_COPIES, FSCK_COUNT_GONE_OK) as $name) {
            $this->counts[$name] = 0;
        }
        $this->found_problems = array();
        foreach (array(FSCK_PROBLEM_NO_COPIES_FOUND, FSCK_PROBLEM_TOO_MANY_COPIES, FSCK_PROBLEM_WRONG_COPY_SIZE, FSCK_PROBLEM_TEMP_FILE) as $name) {
            $this->found_problems[$name] = array();
        }
        if ($reset_dirs) {
            static::$dirs = [];
        }
    }
    public function count($what) {
        $this->counts[$what]++;
    }
    public function found_problem($what, $value = NULL, $key = NULL) {
        if (isset($this->counts[$what])) {
            $this->count($what);
        } elseif (!empty($key)) {
            $this->found_problems[$what][$key] = $value;
        } else {
            $this->found_problems[$what][] = $value;
        }
    }
    public function get_email_body($include_trash_size) {
        if (empty($this->end)) {
            $this->end = time();
        }
        $displayable_duration = duration_to_human($this->end - $this->start);
        $report  = "Scanned directory: " . $this->what . "\n\n";
        $report .= "Started:  " . date('Y-m-d H:i:s', $this->start) . "\n";
        $report .= "Ended:    " . date('Y-m-d H:i:s', $this->end) . "\n";
        $report .= "Duration: $displayable_duration\n\n";
        $report .= "Landing Zone (shares):\n";
        $report .= "  Scanned " . number_format($this->counts[FSCK_COUNT_LZ_DIRS]) . " directories\n";
        $report .= "  Found " . number_format($this->counts[FSCK_COUNT_LZ_FILES]) . " files\n\n";
        if ($this->counts[FSCK_COUNT_META_DIRS] > 0) {
            $report .= "Metadata Store:\n";
            $report .= "  Scanned " . number_format($this->counts[FSCK_COUNT_META_DIRS]) . " directories\n";
            $report .= "  Found " . number_format($this->counts[FSCK_COUNT_META_FILES]) . " files\n";
            $report .= "  Found " . number_format($this->counts[FSCK_COUNT_ORPHANS]) . " orphans\n\n";
        }
        if (empty($this->found_problems[FSCK_PROBLEM_NO_COPIES_FOUND]) && empty($this->found_problems[FSCK_PROBLEM_WRONG_COPY_SIZE])) {
            $report .= "No problems found.\n\n";
        } else {
            $report .= "Problems:\n";
            $problems = $this->found_problems[FSCK_PROBLEM_NO_COPIES_FOUND];
            if (!empty($problems)) {
                $problems = array_unique($problems);
                sort($problems);
                $report .= "  Found " . count($problems) . " files in the metadata store for which no file copies were found.\n";
                if (FsckTask::getCurrentTask()->has_option(OPTION_DEL_ORPHANED_METADATA)) {
                    $report .= "    Those metadata files have been deleted, since you used the --delete-orphaned-metadata option. They will not re-appear in the future.\n";
                } else {
                    $report .= "    Those files were removed from the Landing Zone. (i.e. those files are now gone!) They will re-appear in your shares if a copy re-appear and fsck is run.\n";
                    $report .= "    If you don't want to see those files listed here each time fsck runs, delete the corresponding files from the metadata store using \"greyhole --delete-metadata='<path>'\", where <path> is one of the value listed below.\n";
                }
                $report .= "  Files with no copies:\n";
                $report .= "    " . implode("\n    ", $problems) . "\n\n";
            }
            $problems = $this->found_problems[FSCK_PROBLEM_WRONG_COPY_SIZE];
            if (!empty($problems)) {
                $report .= "  Found " . count($problems) . " file copies with the wrong file size. Those files don't have the same file size as the original files available on your shares. The invalid copies have been moved into the trash.\n";
                foreach ($problems as $real_file_path => $info_array) {
                    $report .= "    $real_file_path is " . number_format($info_array[0]) . " bytes; should be " . number_format($info_array[1]) . " bytes.\n";
                }
                $report .= "\n\n";
            }
        }
        if ($this->counts[FSCK_COUNT_TOO_MANY_COPIES] == 0 && $this->counts[FSCK_COUNT_SYMLINK_TARGET_MOVED] == 0 && count($this->found_problems[FSCK_PROBLEM_TEMP_FILE]) == 0 && $this->counts[FSCK_COUNT_GONE_OK] == 0) {
        } else {
            $report .= "Notices:\n";
            if ($this->counts[FSCK_COUNT_TOO_MANY_COPIES] > 0) {
                $problems = array_unique($this->found_problems[FSCK_PROBLEM_TOO_MANY_COPIES]);
                $report .= "  Found " . $this->counts[FSCK_COUNT_TOO_MANY_COPIES] . " files for which there was too many file copies. Deleted (or moved in trash) files:\n";
                $report .= "    " . implode("\n    ", $problems) . "\n\n";
            }
            if ($this->counts[FSCK_COUNT_SYMLINK_TARGET_MOVED] > 0) {
                $report .= "  Found " . $this->counts[FSCK_COUNT_SYMLINK_TARGET_MOVED] . " files in the Landing Zone that were pointing to a now gone copy.
    Those symlinks were updated to point to the new location of those file copies.\n\n";
            }
            if (count($this->found_problems[FSCK_PROBLEM_TEMP_FILE]) > 0) {
                $problems = $this->found_problems[FSCK_PROBLEM_TEMP_FILE];
                $report .= "  Found " . count($problems) . " temporary files, which are leftovers of interrupted Greyhole executions. The following temporary files were deleted (or moved into the trash):\n";
                $report .= "    " . implode("\n    ", $problems) . "\n\n";
            }
            if ($this->counts[FSCK_COUNT_GONE_OK] > 0) {
                $report .= "  Found " . $this->counts[FSCK_COUNT_GONE_OK] . " missing files that are in a storage pool drive marked Temporarily-Gone.
  If this drive is gone for good, you should execute the following command, and remove the drive from your configuration file:
    greyhole --remove=path
  where path is one of:\n";
                $report .= "    " . implode("\n    ", array_keys(StoragePool::get_gone_ok_drives())) . "\n\n";
            }
        }
        if ($include_trash_size) {
            $report .= "==========\n\n";
            $report .= static::get_trash_size_report();
        }
        return $report;
    }
    public function mergeReport($fsck_report) {
        if (empty(static::$dirs)) {
            $this->start = $fsck_report->start;
        }
        $this->end = @$fsck_report->end;
        static::$dirs[] = $fsck_report->what;
        $this->what = "\n  - " . implode("\n  - ", static::$dirs);
        $this->send_via_email = $fsck_report->send_via_email;
        foreach (array(FSCK_COUNT_META_FILES, FSCK_COUNT_META_DIRS, FSCK_COUNT_LZ_FILES, FSCK_COUNT_LZ_DIRS, FSCK_COUNT_ORPHANS, FSCK_COUNT_SYMLINK_TARGET_MOVED, FSCK_COUNT_TOO_MANY_COPIES, FSCK_COUNT_MISSING_COPIES, FSCK_COUNT_GONE_OK) as $name) {
            $this->counts[$name] += $fsck_report->counts[$name];
        }
        foreach (array(FSCK_PROBLEM_NO_COPIES_FOUND, FSCK_PROBLEM_TOO_MANY_COPIES, FSCK_PROBLEM_WRONG_COPY_SIZE, FSCK_PROBLEM_TEMP_FILE) as $name) {
            if (!empty($fsck_report->found_problems[$name])) {
                $this->found_problems[$name] = array_merge($this->found_problems[$name], $fsck_report->found_problems[$name]);
            }
        }
    }
    public static function get_trash_size_report() {
        $report = "Trash size:\n";
        foreach (Config::storagePoolDrives() as $sp_drive) {
            $trash_path = clean_dir("$sp_drive/.gh_trash");
            if (is_dir($trash_path)) {
                $report .= "  $trash_path = " . trim(exec("du -sh " . escapeshellarg($trash_path) . " | awk '{print $1}'"))."\n";
            } else if (!file_exists($sp_drive)) {
                $report .= "  $sp_drive = N/A\n";
            } else {
                $report .= "  $trash_path = empty\n";
            }
        }
        $report .= "\n";
        return $report;
    }
}
class FSCKWorkLog {
    const FILE = '/usr/share/greyhole/fsck_work_log.dat';
    CONST STATUS_PENDING  = 'pending';
    CONST STATUS_ONGOING  = 'ongoing';
    CONST STATUS_COMPLETE = 'complete';
    public static function initiate($dir, $options, $task_ids) {
        $fsck_work_log = (object) [
            'dir'     => $dir,
            'options' => $options,
            'tasks'   => array(),
        ];
        foreach ($task_ids as $task_id) {
            $fsck_work_log->tasks[] = (object) ['id' => $task_id, 'status' => static::STATUS_PENDING];
        }
        static::saveToDisk($fsck_work_log);
    }
    public static function startTask($task_id) {
        $fsck_work_log = static::getFromDisk();
        foreach ($fsck_work_log->tasks as $task) {
            if ($task->id == $task_id) {
                $task->status = static::STATUS_ONGOING;
                static::saveToDisk($fsck_work_log);
                break;
            }
        }
    }
    public static function isReportAvailable() {
        return file_exists(static::FILE) && !empty(unserialize(file_get_contents(static::FILE)));
    }
    public static function getHumanReadableReport() {
        $fsck_work_log = static::getFromDisk();
        if (count($fsck_work_log->tasks) == 1) {
            $task = first($fsck_work_log->tasks);
            if (empty($task->report)) {
                $task->report = FsckTask::getCurrentTask()->get_fsck_report();
            }
            if (!empty($task->report)) {
                $fsck_report_body = $task->report->get_email_body(FALSE);
            } else {
                $fsck_report_body = "No fsck report available.\n\n";
            }
        } else {
            $report = new FSCKReport(NULL, TRUE);
            foreach ($fsck_work_log->tasks as $task) {
                $report->mergeReport($task->report);
            }
            $fsck_report_body = $report->get_email_body(TRUE);
        }
        return $fsck_report_body;
    }
    public static function taskCompleted($task_id, $send_email) {
        $fsck_work_log = static::getFromDisk();
        foreach ($fsck_work_log->tasks as $task) {
            if ($task->id == $task_id) {
                $task->status = static::STATUS_COMPLETE;
                $fsck_task = FsckTask::getCurrentTask();
                $task->report = $fsck_task->get_fsck_report();
                $task->report->end = time();
                static::saveToDisk($fsck_work_log);
                break;
            }
        }
        $completed_tasks = static::getNumCompletedTasks();
        $total_tasks = count($fsck_work_log->tasks);
        if ($total_tasks > 1) {
            Log::info("Completed $completed_tasks/$total_tasks fsck tasks.");
        }
        if ($completed_tasks == $total_tasks) {
            if ($send_email || Hook::hasHookForEvent(LogHook::EVENT_TYPE_FSCK)) {
                $subject = "[Greyhole] fsck of $fsck_work_log->dir completed on " . exec('hostname');
                $fsck_report_body = static::getHumanReadableReport();
                if ($send_email) {
                    email_sysadmin($subject, $fsck_report_body);
                }
                LogHook::trigger(LogHook::EVENT_TYPE_FSCK, Log::EVENT_CODE_FSCK_REPORT, $subject . "\n" . $fsck_report_body);
            }
        }
    }
    private static function saveToDisk($fsck_work_log) {
        file_put_contents(static::FILE, serialize($fsck_work_log));
    }
    private static function getFromDisk() {
        return unserialize(file_get_contents(static::FILE));
    }
    private static function getNumCompletedTasks() {
        $completed_tasks = 0;
        $fsck_work_log = static::getFromDisk();
        foreach ($fsck_work_log->tasks as $task) {
            if ($task->status == static::STATUS_COMPLETE) {
                $completed_tasks++;
            }
        }
        return $completed_tasks;
    }
}
class FSCKLogFile {
    const PATH = '/usr/share/greyhole';
    private $path;
    private $filename;
    private $lastEmailSentTime = 0;
    public function __construct($filename, $path=self::PATH) {
        $this->filename = $filename;
        $this->path = $path;
    }
    public function emailAsRequired() {
        $logfile = "$this->path/$this->filename";
        if (!file_exists($logfile)) { return; }
        $last_mod_date = filemtime($logfile);
        if ($last_mod_date > $this->getLastEmailSentTime()) {
            $body = $this->getBody(TRUE);
            if ($this->shouldSendViaEmail()) {
                email_sysadmin($this->getSubject(), $body);
            }
            LogHook::trigger(LogHook::EVENT_TYPE_FSCK, Log::EVENT_CODE_FSCK_REPORT, $this->getSubject() . "\n" . $body);
            $this->lastEmailSentTime = $last_mod_date;
            Settings::set("last_email_$this->filename", $this->lastEmailSentTime);
        }
    }
    private function shouldSendViaEmail() {
        $fsck_report = FsckTask::getCurrentTask()->get_fsck_report();
        return (@$fsck_report->send_via_email === TRUE);
    }
    private function getBody($delete_log = false) {
        $logfile = "$this->path/$this->filename";
        if ($this->filename == 'fsck_checksums.log') {
            return file_get_contents($logfile) . "\nNote: You should manually delete the $logfile file once you're done with it.";
        } else if ($this->filename == 'fsck_files.log' && file_exists($logfile)) {
            $fsck_report = unserialize(file_get_contents($logfile));
            if ($delete_log) {
                unlink($logfile);
            }
            return $fsck_report->get_email_body(FALSE) . "\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.";
        } else {
            return '[empty]';
        }
    }
    private function getSubject() {
        if ($this->filename == 'fsck_checksums.log') {
            return '[Greyhole] Mismatched checksums in file copies on ' . exec('hostname');
        } else if ($this->filename == 'fsck_files.log') {
            return '[Greyhole] fsck_files of Greyhole shares on ' . exec('hostname');
        } else {
            return '[Greyhole] Unknown fsck report on ' . exec('hostname');
        }
    }
    private function getLastEmailSentTime() {
        if ($this->lastEmailSentTime == 0) {
            $setting = Settings::get("last_email_$this->filename");
            if ($setting) {
                $this->lastEmailSentTime = (int) $setting;
            }
        }
        return $this->lastEmailSentTime;
    }
    public static function loadFSCKReport($what, $task) {
        $logfile = self::PATH . '/fsck_files.log';
        if (file_exists($logfile)) {
            $fsck_report = unserialize(file_get_contents($logfile));
            $task->set_fsck_report($fsck_report);
            if ($fsck_report !== FALSE) {
                return;
            }
            rename($logfile, "$logfile.broken");
        }
        $task->initialize_fsck_report($what);
    }
    public static function saveFSCKReport($send_via_email, $task) {
        $fsck_report = $task->get_fsck_report();
        $fsck_report->send_via_email = $send_via_email;
        $logfile = self::PATH . '/fsck_files.log';
        file_put_contents($logfile, serialize($fsck_report));
    }
}
function fix_all_symlinks() {
    foreach (SharesConfig::getShares() as $share_name => $share_options) {
        fix_symlinks_on_share($share_name);
    }
}
function fix_symlinks_on_share($share_name) {
    $share_options = SharesConfig::getConfigForShare($share_name);
    echo "Looking for broken symbolic links in the share '$share_name'...";
    chdir($share_options[CONFIG_LANDING_ZONE]);
    exec("find -L . -type l", $result);
    foreach ($result as $file_to_relink) {
        if (is_link($file_to_relink)) {
            $file_to_relink = substr($file_to_relink, 2);
            foreach (Config::storagePoolDrives() as $sp_drive) {
                if (!StoragePool::is_pool_drive($sp_drive)) { continue; }
                $new_link_target = clean_dir("$sp_drive/$share_name/$file_to_relink");
                if (gh_is_file($new_link_target)) {
                    unlink($file_to_relink);
                    gh_symlink($new_link_target, $file_to_relink);
                    break;
                }
            }
        }
    }
    echo " Done.\n";
}
function schedule_fsck_all_shares($fsck_options=array()) {
    $task_ids = array();
    foreach (SharesConfig::getShares() as $share_name => $share_options) {
        $query = "INSERT INTO tasks SET action = 'fsck', share = :full_path, additional_info = :fsck_options, complete = 'yes'";
        $params = array(
            'full_path' => $share_options[CONFIG_LANDING_ZONE],
            'fsck_options' => empty($fsck_options) ? NULL : implode('|', $fsck_options)
        );
        $task_ids[] = DB::insert($query, $params);
    }
    return $task_ids;
}
function kshift(&$arr) {
    if (empty($arr)) {
        return FALSE;
    }
    foreach ($arr as $k => $v) {
        unset($arr[$k]);
        break;
    }
    return array($k, $v);
}
function array_contains($haystack, $needle) {
    return array_search($needle, $haystack) !== FALSE;
}
function string_contains($haystack, $needle) {
    if (!is_string($haystack) || empty($haystack)) {
        return FALSE;
    }
    return mb_strpos($haystack, $needle) !== FALSE;
}
function string_starts_with($haystack, $needle) {
    if (!is_string($haystack) || empty($haystack)) {
        return FALSE;
    }
    if (is_array($needle)) {
        foreach ($needle as $n) {
            if (string_starts_with($haystack, $n)) {
                return TRUE;
            }
        }
        return FALSE;
    }
    return mb_strpos($haystack, $needle) === 0;
}
function string_ends_with($haystack, $needle) {
    if (!is_string($haystack) || empty($haystack)) {
        return FALSE;
    }
    if (is_array($needle)) {
        foreach ($needle as $n) {
            if (string_ends_with($haystack, $n)) {
                return TRUE;
            }
        }
        return FALSE;
    }
    return ( substr(strtolower($haystack), -strlen($needle)) == strtolower($needle) );
}
function json_pretty_print($json) {
    if (defined('JSON_PRETTY_PRINT')) {
        if (is_string($json)) {
            $json = json_decode($json);
        }
        return json_encode($json, JSON_PRETTY_PRINT);
    }
    if (!is_string($json)) {
        $json = json_encode($json);
    }
    $result = '';
    $level = 0;
    $in_quotes = FALSE;
    $in_escape = FALSE;
    $ends_line_level = NULL;
    $json_length = strlen( $json );
    for ($i = 0; $i < $json_length; $i++) {
        $char = $json[$i];
        $new_line_level = NULL;
        $post = "";
        if ($ends_line_level !== NULL) {
            $new_line_level = $ends_line_level;
            $ends_line_level = NULL;
        }
        if ($in_escape) {
            $in_escape = FALSE;
        } else if ($char === '"') {
            $in_quotes = !$in_quotes;
        } else if (!$in_quotes) {
            switch($char) {
                case '}':
                case ']':
                    $level--;
                    $ends_line_level = NULL;
                    $new_line_level = $level;
                    break;
                case '{':
                case '[':
                    $level++;
                case ',':
                    $ends_line_level = $level;
                    break;
                case ':':
                    $post = " ";
                    break;
                case " ":
                case "\t":
                case "\n":
                case "\r":
                    $char = "";
                    $ends_line_level = $new_line_level;
                    $new_line_level = NULL;
                    break;
            }
        } else if ($char === '\\') {
            $in_escape = true;
        }
        if ($new_line_level !== NULL) {
            $result .= "\n" . str_repeat("  ", $new_line_level);
        }
        $result .= $char . $post;
    }
    return $result;
}
function gh_wild_mb_strpos($haystack, $needle) {
    $is_wild = string_contains($needle, "*");
    if (!$is_wild) {
        return mb_strpos($haystack, $needle);
    }
    if (str_replace('*', '', $needle) == $haystack) {
        return FALSE;
    }
    $needles = explode("*", $needle);
    if ($needle[0] == '*') {
        $first_index = 0;
    }
    $found = FALSE;
    foreach ($needles as $needle_part) {
        if ($needle_part == '') {
            continue;
        }
        $needle_index = mb_strpos($haystack, $needle_part);
        if (!isset($first_index)) {
            $first_index = $needle_index;
        }
        if ($needle_index === FALSE) {
            return FALSE;
        } else {
            $found = TRUE;
            $haystack = mb_substr($haystack, $needle_index + mb_strlen($needle_part));
        }
    }
    if ($found) {
        return $first_index;
    }
    return FALSE;
}
function str_replace_first($search, $replace, $subject) {
    $firstChar = mb_strpos($subject, $search);
    if ($firstChar !== FALSE) {
        $beforeStr = mb_substr($subject, 0, $firstChar);
        $afterStr = mb_substr($subject, $firstChar + mb_strlen($search));
        return $beforeStr . $replace . $afterStr;
    } else {
        return $subject;
    }
}
function normalize_utf8_characters($string) {
    return normalizer_normalize($string);
}
function spawn_thread($action, $arguments) {
    $num_worker_thread = (int) exec('ps ax | grep "/usr/bin/greyhole --' . $action . '" | grep "drive='. implode('" | grep "drive=', $arguments) . '" | grep -v grep | grep -v bash | wc -l');
    if ($num_worker_thread > 0) {
        Log::debug("Won't spawn a duplicate thread; 'greyhole --$action --drive=$arguments[0]' is already running");
        return 1;
    }
    $cmd = "/usr/bin/greyhole --$action --drive=" . implode(' --drive=', array_map('escapeshellarg', $arguments));
    exec("$cmd 1>/var/run/greyhole_thread.pid 2>&1 &");
    usleep(100000); // 1/10s
    return (int) file_get_contents('/var/run/greyhole_thread.pid');
}
function to_object($o) {
    if (is_array($o)) {
        return (object) $o;
    }
    return $o;
}
function first($array, $default=NULL) {
    if (is_iterable($array)) {
        foreach ($array as $el) {
            return $el;
        }
    }
    return $default;
}
if (!function_exists('is_iterable')) {
    function is_iterable($var) {
        return is_array($var) || $var instanceof Traversable;
    }
}
function log_file_checksum($share, $full_path, $checksum) {
    $q = "INSERT INTO checksums SET id = :id, share = :share, full_path = :full_path, checksum = :checksum ON DUPLICATE KEY UPDATE checksum = VALUES(checksum)";
    DB::insert(
        $q,
        [
            'id'        => md5(clean_dir("$share/$full_path")),
            'share'     => $share,
            'full_path' => $full_path,
            'checksum'  => $checksum,
        ]
    );
}
function get_share_and_fullpath_from_realpath($real_path) {
    $prefix = StoragePool::getDriveFromPath($real_path);
    if (!$prefix) {
        $share_options = SharesConfig::getShareOptions($real_path);
        $lz = $share_options['landing_zone'];
        $array = explode('/', $lz);
        array_pop($array);
        $prefix = implode('/', $array);
    }
    $share_and_fullpath = substr($real_path, strlen($prefix)+1);
    $array = explode('/', $share_and_fullpath);
    $share = array_shift($array);
    $full_path = implode('/', $array);
    return array($share, $full_path);
}
function how_long_ago($past_time) {
    $ago = '';
    $s = time() - $past_time;
    $m = floor($s / 60);
    if ($m > 0) {
        $s -= $m * 60;
        $h = floor($m / 60);
        if ($h > 0) {
            $ago = $h . "h ";
            $m -= $h * 60;
        }
        $ago = $ago . $m . "m ";
    }
    $ago = $ago . $s . "s";
    if ($ago == '0s') {
        return 'just now';
    }
    return "$ago ago";
}
function to_array($el) {
    if (is_array($el)) {
        return $el;
    }
    return [$el];
}
function get_config_hash() {
    exec("cat " . escapeshellarg(ConfigHelper::$config_file) . " | grep -v '^\s*#' | grep -v '^\s*$'", $output);
    $output = array_map('trim', $output);
    return md5(implode("\n", $output));
}
function get_config_hash_samba() {
    exec("/usr/bin/testparm -ls 2>/dev/null", $output);
    $output = array_map('trim', $output);
    return md5(implode("\n", $output));
}

class CliCommandDefinition {
    protected $longOpt;
    protected $opt;
    protected $paramName;
    private $cliRunnerClass;
    protected $help;
    function __construct($longOpt, $opt, $paramName, $cliRunnerClass, $help) {
        $this->longOpt = $longOpt;
        $this->opt = $opt;
        $this->paramName = $paramName;
        $this->cliRunnerClass = $cliRunnerClass;
        $this->help = $help;
    }
    public function getNewRunner($options) {
        if (empty($this->cliRunnerClass)) {
            return FALSE;
        }
        return new $this->cliRunnerClass($options, $this);
    }
    public function getOpt() {
        return $this->opt;
    }
    public function getLongOpt() {
        return $this->longOpt;
    }
    public function paramSpecified($command_line_options) {
        $simple_opt = str_replace(':', '', $this->opt);
        $simple_long_opt = str_replace(':', '', $this->longOpt);
        $options = array($simple_opt, $simple_long_opt);
        $keys = array_keys($command_line_options);
        foreach ($options as $opt) {
            if (array_contains($keys, $opt)) {
                if (empty($command_line_options[$opt])) {
                    return TRUE;
                }
                return $command_line_options[$opt];
            }
        }
        return FALSE;
    }
    public function getUsage() {
        if (empty($this->help)) {
            return '';
        }
        $simple_opt = str_replace(':', '', $this->opt);
        $simple_long_opt = str_replace(':', '', $this->longOpt);
        $full_width = 80;
        $prefix_length = 24;
        $padded_newline = "\n" . str_repeat(' ', $prefix_length);
        $simple_opt_usage = '';
        if (!empty($simple_opt)) {
            $simple_opt_usage = "-$simple_opt, ";
        }
        $prefix = sprintf("%-" . $prefix_length . "s", "  $simple_opt_usage--$simple_long_opt" . (!empty($this->paramName) ? $this->paramName : ''));
        if (strlen($prefix) > $prefix_length) {
            $prefix .= $padded_newline;
        }
        $help = wordwrap(str_replace("\n", $padded_newline, $this->help), $full_width-$prefix_length, $padded_newline);
        return $prefix . $help . "\n";
    }
}

class CliOptionDefinition extends CliCommandDefinition {
    function __construct($longOpt, $opt, $paramName, $help) {
        parent::__construct($longOpt, $opt, $paramName, null, $help);
    }
}

abstract class AbstractRunner {
	function __construct() {
	}
	abstract public function run();
	public function canRun() {
		if (exec("/usr/bin/whoami") != 'root') {
			return FALSE;
		}
		return TRUE;
	}
	public function finish($returnValue = 0) {
		exit($returnValue);
	}
}

abstract class AbstractCliRunner extends AbstractRunner {
    protected $options;
    protected $cli_command;
    function __construct($options, $cli_command) {
        parent::__construct();
        $this->options = $options;
        $this->cli_command = $cli_command;
    }
    protected function log($what='') {
        echo "$what\n";
    }
    protected function logn($what) {
        echo "$what";
    }
    protected function restart_service() {
        if (!DaemonRunner::restart_service()) {
            $this->log("You should now restart the Greyhole daemon.");
        }
    }
    protected function parseCmdParamAsDriveAndExpect($expectedParamValues) {
        if (isset($this->options['cmd_param'])) {
            $dir = $this->options['cmd_param'];
            if (!array_contains($expectedParamValues, $dir)) {
                $dir = '/' . trim($dir, '/');
            }
        }
        if (empty($dir) || !array_contains($expectedParamValues, $dir)) {
            return FALSE;
        }
        return $dir;
    }
}

class BalanceCliRunner extends AbstractCliRunner {
    public function run() {
        $query = "INSERT INTO tasks (action, share, complete) VALUES ('balance', '', 'yes')";
        DB::insert($query);
        $this->log("A balance has been scheduled. It will start after all currently pending tasks have been completed.");
        $this->log("This operation will try to even the available space on all drives included in your storage pool.");
    }
}

abstract class AbstractAnonymousCliRunner extends AbstractCliRunner {
    public function canRun() {
        return TRUE;
    }
}

class BalanceStatusCliRunner extends AbstractAnonymousCliRunner {
    private $refresh_interval = 15;
    public function run() {
        while (TRUE) {
            $num_lines = $this->output();
            sleep($this->refresh_interval);
            for ($i=0; $i < $num_lines; $i++) {
                echo "\r\033[K\033[1A\r\033[K\r";
            }
        }
    }
    public function output() {
        $num_lines = 0;
        $cols = exec('tput cols');
        printf("Watching every %ds:%s%s\n", $this->refresh_interval, str_repeat(' ', $cols - 50), date('r'));
        $num_lines++;
        $max_storage_pool_strlen = max(array_map('mb_strlen', Config::storagePoolDrives()));
        $cols -= $max_storage_pool_strlen + 14;
        $groups = static::getData();
        foreach ($groups as $group) {
            printf("\n%$max_storage_pool_strlen"."s  %s", "", "Target free space in $group->name storage pool drives: " . bytes_to_human($group->target_avail_space*1024, FALSE) . "\n");
            $num_lines += 2;
            foreach ($group->drives as $sp_drive => $drive_infos) {
                $cols_free = ceil($cols * $drive_infos->percent_free);
                $cols_used = $cols - abs($cols_free);
                $suffix = "\033[0m";
                $cols_diff = round($cols * $drive_infos->percent_diff);
                if ($drive_infos->direction == '-') {
                    $cols_used -= $cols_diff;
                    $prefix = "\033[31m";
                } else {
                    $cols_free -= $cols_diff;
                    $prefix = "\033[32m";
                }
                $how_much = round($drive_infos->diff / 1024 / 1024) . 'GB';
                $sign = $drive_infos->direction;
                $cols_used = max($cols_used, 0);
                $cols_diff = max($cols_diff, 0);
                $cols_free = max($cols_free, 0);
                printf("%$max_storage_pool_strlen"."s  [%s%s%s%s%s]  %s %6s\n", $sp_drive, str_repeat(mb_convert_encoding('&#9724;', 'UTF-8', 'HTML-ENTITIES'), $cols_used), $prefix, str_repeat(mb_convert_encoding('&#9724;', 'UTF-8', 'HTML-ENTITIES'), $cols_diff), $suffix, str_repeat(' ', $cols_free), $sign, $how_much);
                $num_lines++;
            }
        }
        return $num_lines;
    }
    public static function getData() {
        $groups = [];
        $drives_selectors = Config::get(CONFIG_DRIVE_SELECTION_ALGORITHM);
        foreach ($drives_selectors as $ds) {
            $pool_drives_avail_space = StoragePool::get_drives_available_space();
            foreach ($pool_drives_avail_space as $drive => $available_space) {
                if (!array_contains($ds->drives, $drive)) {
                    unset($pool_drives_avail_space[$drive]);
                    continue;
                }
            }
            if (count($pool_drives_avail_space) == 0) {
                continue;
            }
            $target_avail_space = array_sum($pool_drives_avail_space) / count($pool_drives_avail_space);
            $group = (object) [
                'name' => $ds->group_name,
                'target_avail_space' => $target_avail_space,
                'drives' => [],
            ];
            $groups[] = $group;
            foreach (Config::storagePoolDrives() as $sp_drive) {
                if (!array_contains($ds->drives, $sp_drive)) {
                    continue;
                }
                $df = StoragePool::get_free_space($sp_drive);
                if (!$df) {
                    continue;
                }
                $df['free'] -= (float) Config::get(CONFIG_MIN_FREE_SPACE_POOL_DRIVE, $sp_drive);
                $drive_infos = (object) [
                    'df' => $df,
                    'percent_free' => $df['free'] / ($df['free'] + $df['used']),
                ];
                $group->drives[$sp_drive] = $drive_infos;
                if ($df['free'] < $target_avail_space) {
                    $drive_infos->direction = '-';
                    $drive_infos->diff = $target_avail_space - $df['free'];
                } else {
                    $drive_infos->direction = '+';
                    $drive_infos->diff = $df['free'] - $target_avail_space;
                }
                $drive_infos->percent_diff = $drive_infos->diff / ($df['free'] + $df['used']);
            }
        }
        return $groups;
    }
}

class BootInitCliRunner extends AbstractCliRunner {
    public function run() {
	    $query = "UPDATE tasks SET complete = 'yes' WHERE complete = 'no' AND action = 'write'";
        DB::execute($query);
    }
}

class CancelBalanceCliRunner extends AbstractCliRunner {
    public function run() {
        DB::execute("DELETE FROM tasks WHERE action = 'balance'");
        $this->log("All scheduled balance tasks have now been deleted.");
        $this->restart_service();
    }
}

class CancelFsckCliRunner extends AbstractCliRunner {
    public function run() {
        DB::execute("DELETE FROM tasks WHERE action IN ('fsck', 'md5')");
        $this->log("All scheduled fsck tasks have now been deleted.");
        $this->log("Specific files checks might have been queued for problematic files, and those (fsck_file) tasks will still be executed, once other tasks have been processed.");
        $this->restart_service();
    }
}

class ConfigCliRunner extends AbstractCliRunner {
    public function run() {
        $argc = $GLOBALS['argc'];
        $argv = $GLOBALS['argv'];
        if ($argc != 3 && $argc != 4) {
            echo "Usage: greyhole --config name [value]\n";
            exit(1);
        }
        $name = $argv[2];
        if ($argc > 3) {
            $value = $argv[3];
            static::change_config($name, $value, [$this, 'log']);
        } else {
            $config_value = Config::get($name);
            $this->log(json_encode($config_value));
        }
    }
    public static function change_config($name, $value, $log_fct, &$error = NULL) {
        if (empty($log_fct)) {
            $log_fct = function($log) { error_log($log); };
        }
        $config_file = ConfigHelper::$config_file;
        if (string_starts_with($name, 'smb.conf:')) {
            $config_file = ConfigHelper::$smb_config_file;
            if (!preg_match('/smb.conf:\[(.+)](.+)$/', $name, $re)) {
                $error = "Invalid format for option name: $name";
                return;
            }
            $section = $re[1];
            $name = $re[2];
        }
        if (string_starts_with($name, 'num_copies') && empty($value)) {
            $value = '___REMOVE___';
        }
        if ($name == 'ignored_folders' || $name == 'ignored_files') {
            $content = file_get_contents(ConfigHelper::$config_file);
            $content = explode("\n", $content);
            foreach ($content as $i => $line) {
                if (preg_match('/\s*(.+)\s*=\s*(.*)$/', $line, $re) && trim($re[1]) == $name) {
                    $content[$i] = "#$line";
                    $log_fct("Commented-out $name = $re[2] at line " . ($i+1) . " in " . ConfigHelper::$config_file . "");
                }
            }
            $ignore_values = explode("\n", $value);
            foreach ($content as $i => $line) {
                if (preg_match('/#\s*(.+)\s*=\s*(.*)$/', $line, $re) && trim($re[1]) == $name) {
                    foreach ($ignore_values as $j => $ignore_value) {
                        if ($re[2] == $ignore_value) {
                            $content[$i] = substr($line, 1);
                            $log_fct("Keeping $name = $ignore_value at line " . ($i+1) . " in " . ConfigHelper::$config_file . "");
                            unset($ignore_values[$j]);
                            break;
                        }
                    }
                }
            }
            foreach ($ignore_values as $ignore_value) {
                if (empty(trim($ignore_value))) { continue; }
                $content[] = "\t$name = $ignore_value";
                $log_fct("Will append to " . ConfigHelper::$config_file . ": $name = $ignore_value");
            }
            $content = implode("\n", $content) . (!empty($ignore_values) ? "\n" : "");
            file_put_contents(ConfigHelper::$config_file, $content);
            exit(0);
        }
        if (string_starts_with($name, 'min_free_space_pool_drive')) {
            $sp_drive = substr($name, 26, -1);
            $name = "storage_pool_drive";
            $needs_match = "@$sp_drive\s*,\s*min_free\s*:\s*\d+@";
            $value = "$sp_drive, min_free: $value";
        }
        if (string_starts_with($name, 'drive_selection_groups')) {
            $group_name = substr($name, 23, -1);
            $name = ['drive_selection_groups', ''];
            $needs_match = "@[=\s]$group_name:\s*@";
            $value = "$group_name: " . str_replace(',/', ', /', $value);
        }
        exec("cat " . escapeshellarg($config_file) . " | grep -n -v '^\s*[#;]' | grep -v '^[0-9]*:\s*$'", $output);
        if (!empty($section)) {
            $output = static::filter_lines_for_section($output, $section);
        }
        foreach ($output as $line) {
            $equal_optional = '';
            if (array_contains(to_array($name), '')) {
                $equal_optional = '?';
            }
            if (preg_match('/(\d+):\s*(.+)\s*='.$equal_optional.'\s*(.*)$/U', $line, $re) && array_contains(to_array($name), trim($re[2])) && (empty($needs_match) || preg_match($needs_match, $line))) {
                $line_number = $re[1];
                $log_fct("Will overwrite line $line_number in " . $config_file . ':');
                break;
            }
        }
        if (empty($line_number)) {
            unset($output);
            exec("cat " . escapeshellarg($config_file) . " | egrep -n '^\s*[#;]|^\s*\[' | grep -v '^[0-9]*:\s*$'", $output);
            if (!empty($section)) {
                $output = static::filter_lines_for_section($output, $section);
            }
            foreach ($output as $line) {
                if (preg_match('/(\d+):[#;]\s*(.+)\s*=\s*(.*)$/U', $line, $re) && array_contains(to_array($name), trim($re[2])) && (empty($needs_match) || preg_match($needs_match, $line))) {
                    $line_number = $re[1];
                    $log_fct("Will overwrite line $line_number in " . $config_file . ':');
                    break;
                }
            }
        }
        $name = first(to_array($name));
        $content = file_get_contents($config_file);
        if (empty($line_number)) {
            if ($name == "storage_pool_drive") {
                if (!Config::get(CONFIG_ALLOW_MULTIPLE_SP_PER_DRIVE)) {
                }
                if (!is_dir($sp_drive) && is_dir(dirname($sp_drive))) {
                    mkdir($sp_drive, 0777);
                }
                if (!is_dir($sp_drive)) {
                    $error = "Specified path '$sp_drive' does not exist.";
                    return;
                }
            }
            $log_fct("Will append to " . $config_file . ':');
            if (!empty($section)) {
                $content .= "\n[$section]";
            }
            $prefix = '';
            if ($value === '___REMOVE___') {
                $prefix = "#";
            }
            $content .= "\n$prefix$name = $value\n";
        } else {
            $content = explode("\n", $content);
            $before = $content[$line_number-1];
            if ($value === '___REMOVE___') {
                if ($before[0] != '#' && $before[0] != ';') {
                    $content[$line_number - 1] = "#$before";
                }
            } else {
                if ($before[0] == '#' || $before[0] == ';') {
                    $before[0] = ' ';
                }
                $prefix = '';
                if (preg_match('/^(\s*)/', $before, $re)) {
                    $prefix = $re[1];
                }
                $after = "$prefix$name = $value";
                $content[$line_number-1] = $after;
            }
            $content = implode("\n", $content);
        }
        file_put_contents($config_file, $content);
        $log_fct("$name = $value");
    }
    private static function filter_lines_for_section($lines, $section) {
        $found_section = FALSE;
        foreach ($lines as $i => $line) {
            if (preg_match('/\[(.+)]/', $line, $re)) {
                $found_section = ( trim($re[1]) == $section );
                unset($lines[$i]);
                continue;
            }
            if (!$found_section) {
                unset($lines[$i]);
            }
        }
        return array_values($lines);
    }
}

class CopyCliRunner extends AbstractCliRunner {
    protected $source;
    protected $share_name;
    protected $target;
    public function run() {
        ConfigHelper::parse();
        DB::connect();
        Log::setAction(ACTION_INITIALIZE);
        Metastores::choose_metastores_backups();
        Log::setAction(ACTION_CP);
        Log::setLevel(Log::INFO);
        $argc = $GLOBALS['argc'];
        $argv = $GLOBALS['argv'];
        if (basename($argv[0]) == 'cpgh') {
            $num_required_args = 3;
        } else {
            $num_required_args = 4;
        }
        if ($argc != $num_required_args) {
            error_log(
                "\n"
                . "Usage: cpgh source share_name/target/dir/\n"
                . "       greyhole --cp source share_name/target/dir/\n"
                . "\n"
                . "Examples: cpgh \"Some Movie (2021)\" Videos/Movies/\n"
                . "          greyhole --cp \"Something Large\" Backups/\n"
                . "\n"
                . "`cpgh` is used to ADD files onto your Greyhole storage pool, without going through Samba.\n"
                . "\n"
                . "Instead of copying the files into a Samba share, and letting the Greyhole daemon then\n"
                . "  move the files into one of your storage pool drives, this command will copy the SOURCE\n"
                . "  files directly into a storage pool drive.\n"
                . "  It will also create extra copies of those files, if the TARGET share is configured with\n"
                . "  num_copies > 1.\n"
            );
            exit(1);
        }
        $source = $argv[$num_required_args-2];
        $target = $argv[$num_required_args-1];
        if (!file_exists($source)) {
            error_log("cpgh: cannot access '$source': No such file or directory");
            if (getenv('IN_DOCKER')) {
                error_log("\nNote that since you are using Docker, the 'source' file/folder needs to be accessible within the Docker container.");
            }
            exit(2);
        }
        $target = explode('/', $target);
        $share_name = array_shift($target);
        $target = clean_dir(implode('/', $target) . '/' . basename($source));
        if (is_dir($source)) {
            $target .= '/';
        }
        if (!SharesConfig::exists($share_name)) {
            error_log("cpgh: target '$share_name' is not a valid Samba/Greyhole share.");
            exit(3);
        }
        echo "Source" . (is_dir($source) ? ' (folder)' : '') . ": $source\n";
        echo "Target share: $share_name\n";
        echo "Target in share: $target\n";
        $this->source = $source;
        $this->share_name = $share_name;
        $this->target = $target;
        Config::set(CONFIG_CHECK_FOR_OPEN_FILES, FALSE);
        StorageFile::override_file_permissions();
        if (is_dir($source)) {
            static::glob_dir($source);
        } else {
            static::copy_file($source);
        }
        echo "\n";
    }
    protected function copy_file($file) {
        $file = clean_dir($file);
        $target_full_path = $this->target . str_replace($this->source, '', $file);
        $target_full_path = clean_dir(trim($target_full_path, '/'));
        $target_full_path = str_replace([':', '?', '*', '<', '>', '|'], '-', $target_full_path);
        $t = new WriteTask(
            [
                'id'              => 0,
                'action'          => 'write',
                'share'           => $this->share_name,
                'full_path'       => $target_full_path,
                'additional_info' => 'source:' . $file,
                'complete'        => 'yes',
                'event_date'      => date('Y-m-d H:i:s'),
            ]
        );
        echo "\n";
        $t->execute();
    }
    protected function glob_dir($dir) {
        $dir = new DirectoryIterator($dir);
        foreach ($dir as $fileinfo) {
            if ($fileinfo->isDot()) {
                continue;
            }
            $file = $fileinfo->getPathname();
            if ($fileinfo->isDir()) {
                static::glob_dir($file);
            } else {
                static::copy_file($file);
            }
        }
    }
}

class CreateMemSpoolRunner extends AbstractCliRunner {
    public function run() {
        if (SambaSpool::create_mem_spool() && SambaSpool::create_mem_spool()) {
            $this->log("There was a problem creating the in-memory spool folder. Check your Greyhole log for details.");
        } else {
            $this->log("The in-memory spool folder is now correctly mounted and ready to be used by the Samba VFS module.");
        }
    }
}

class DebugCliRunner extends AbstractAnonymousCliRunner {
    public function run() {
        if (!isset($this->options['cmd_param'])) {
            $this->log("Please specify a file to debug.");
            $this->finish(1);
        }
        $filename = $this->options['cmd_param'];
        if (!string_contains($filename, '/')) {
            $filename = "/$filename";
        }
        $this->log("Debugging file operations for file named \"$filename\"");
        $this->log();
        list($to_grep, $debug_tasks) = $this->getDBLogs($filename);
        $this->getAppLogs($to_grep);
        $this->getFilesystemDetails($debug_tasks);
    }
    private function getDBLogs($filename) {
        $this->log("From DB");
        $this->log("=======");
        $query = "SELECT id, action, share, full_path, additional_info, event_date FROM tasks_completed WHERE full_path LIKE :filename ORDER BY id";
        $debug_tasks = DB::getAll($query, array('filename' => "%$filename%"), 'id');
        $query = "SELECT id, action, share, full_path, additional_info, event_date FROM tasks_completed WHERE additional_info LIKE :filename ORDER BY id";
        $params = array('filename' => "%$filename%");
        while (TRUE) {
            $rows = DB::getAll($query, $params);
            foreach ($rows as $row) {
                $debug_tasks[$row->id] = $row;
                $query = "SELECT id, action, share, full_path, additional_info, event_date FROM tasks_completed WHERE additional_info = :full_path ORDER BY id";
                $params = array('full_path' => $row->full_path);
            }
            $new_query = preg_replace('/SELECT .* FROM/i', 'SELECT COUNT(*) FROM', $query);
            $count = DB::getFirstValue($new_query, $params);
            if ($count == 0) {
                break;
            }
        }
        ksort($debug_tasks);
        $to_grep = array();
        foreach ($debug_tasks as $task) {
            $this->log("[$task->event_date] Task ID $task->id: $task->action $task->share/$task->full_path" . ($task->action == 'rename' ? " -> $task->share/$task->additional_info" : ''));
            $to_grep["$task->share/$task->full_path"] = 1;
            if ($task->action == 'rename') {
                $to_grep["$task->share/$task->additional_info"] = 1;
            }
        }
        if (empty($to_grep)) {
            $to_grep[$filename] = 1;
            if (string_contains($filename, '/')) {
                $share = trim(mb_substr($filename, 0, mb_strpos(mb_substr($filename, 1), '/')+1), '/');
                $full_path = trim(mb_substr($filename, mb_strpos(mb_substr($filename, 1), '/')+1), '/');
                $debug_tasks[] = (object) array('share' => $share, 'full_path' => $full_path);
            }
        }
        return array($to_grep, $debug_tasks);
    }
    private function getAppLogs($to_grep) {
        $this->log();
        $this->log("From logs");
        $this->log("=========");
        $to_grep = array_keys($to_grep);
        $to_grep = implode("|", $to_grep);
        $commands = array();
        $commands[] = "zgrep -h -E -B 1 -A 2 -h " . escapeshellarg($to_grep) . " " . Config::get(CONFIG_GREYHOLE_LOG_FILE) . "*.gz";
        $commands[] = "grep -h -E -B 1 -A 2 -h " . escapeshellarg($to_grep) . " " . escapeshellarg(Config::get(CONFIG_GREYHOLE_LOG_FILE));
        foreach ($commands as $command) {
            exec($command, $result);
        }
        $result2 = array();
        $i = 0;
        foreach ($result as $rline) {
            if ($rline == '--') { continue; }
            $date_time = substr($rline, 0, 15);
            $timestamp = strtotime($date_time);
            $result2[$timestamp.sprintf("%04d", $i++)] = $rline;
        }
        ksort($result2);
        $this->log(implode("\n", $result2));
    }
    private function getFilesystemDetails($debug_tasks) {
        $this->log();
        $this->log("From filesystem");
        $this->log("===============");
        $last_task = array_pop($debug_tasks);
        $share = $last_task->share;
        $full_path = $last_task->full_path;
        $this->log("Landing Zone:");
        passthru("ls -l " . escapeshellarg(get_share_landing_zone($share) . "/" . $full_path));
        $this->log();
        $this->log("Metadata store:");
        foreach (Config::storagePoolDrives() as $sp_drive) {
            $metastore = clean_dir("$sp_drive/" . Metastores::METASTORE_DIR);
            if (file_exists("$metastore/$share/$full_path")) {
                passthru("ls -l " . escapeshellarg("$metastore/$share/$full_path"));
                $data = unserialize(file_get_contents("$metastore/$share/$full_path"));
                $this->log(json_pretty_print($data));
            }
        }
        $this->log();
        $this->log("File copies:");
        foreach (Config::storagePoolDrives() as $sp_drive) {
            if (file_exists("$sp_drive/$share/$full_path")) {
                $this->logn("  "); passthru("ls -l " . escapeshellarg("$sp_drive/$share/$full_path"));
            }
        }
    }
}

class DeleteMetadataCliRunner extends AbstractCliRunner {
    private $dir;
    function __construct($options, $cli_command) {
        parent::__construct($options, $cli_command);
        if (!isset($this->options['cmd_param'])) {
            $this->log("Please specify the path to a file that is gone from your storage pool. Eg. 'Movies/HD/The Big Lebowski.mkv'");
            $this->finish(1);
        }
        $this->dir = $this->options['cmd_param'];
    }
    public function run() {
        $share = trim(mb_substr($this->dir, 0, mb_strpos($this->dir, '/')+1), '/');
        $full_path = trim(mb_substr($this->dir, mb_strpos($this->dir, '/')+1), '/');
        list($path, $filename) = explode_full_path($full_path);
        Metastores::choose_metastores_backups();
        foreach (Metastores::get_metafile_data_filenames($share, $path, $filename) as $file) {
            if (file_exists($file)) {
                $this->log("Deleting $file");
                unlink($file);
            }
        }
        $this->log("Done.");
    }
}

class EmptyTrashCliRunner extends AbstractCliRunner {
    public function run() {
        foreach (Config::storagePoolDrives() as $sp_drive) {
            $trash_path = clean_dir("$sp_drive/.gh_trash");
            if (!file_exists($trash_path)) {
                $this->log("Trash in $sp_drive is empty. Nothing to do.");
            } else {
                $trash_size = trim(exec("du -sk " . escapeshellarg($trash_path) . " | awk '{print $1}'"));
                $this->logn("Trash in $sp_drive is " . bytes_to_human($trash_size*1024, FALSE) . ". Emptying... ");
                exec("rm -rf " . escapeshellarg($trash_path));
                $this->log("Done");
            }
        }
        $trash_share = SharesConfig::getConfigForShare(CONFIG_TRASH_SHARE);
        if ($trash_share && mb_strlen(escapeshellarg($trash_share[CONFIG_LANDING_ZONE])) > 8) {
            exec("rm -rf " . escapeshellarg($trash_share[CONFIG_LANDING_ZONE]) . '/*');
        }
    }
    protected function log($what='') {
        if (isset($_POST)) {
            error_log($what);
        } else {
            echo "$what\n";
        }
    }
    protected function logn($what) {
        if (isset($_POST)) {
            error_log($what);
        } else {
            echo $what;
        }
    }
}

class FixSymlinksCliRunner extends AbstractCliRunner {
    public function run() {
        fix_all_symlinks();
    }
}

define('OPTION_EMAIL', 'email');
define('OPTION_IF_CONF_CHANGED', 'if-conf-changed');
define('OPTION_CHECKSUMS','checksums');
define('OPTION_SKIP_METASTORE','skip-metastore');
define('OPTION_ORPHANED','orphaned');
define('OPTION_DU','du');
define('OPTION_DEL_ORPHANED_METADATA','del-orphaned-metadata');
define('OPTION_VALIDATE_COPIES','validate-copies-created');
class FsckCliRunner extends AbstractCliRunner {
    private $dir = '';
    private $fsck_options = array();
    private static $available_options = array(
       'email-report' => OPTION_EMAIL,
       'dont-walk-metadata-store' => OPTION_SKIP_METASTORE,
       'if-conf-changed' => OPTION_IF_CONF_CHANGED,
       'disk-usage-report' => OPTION_DU,
       'find-orphaned-files' => OPTION_ORPHANED,
       'checksums' => OPTION_CHECKSUMS,
       'delete-orphaned-metadata' => OPTION_DEL_ORPHANED_METADATA
    );
    function __construct($options, $cli_command) {
        parent::__construct($options, $cli_command);
        if (isset($this->options['dir'])) {
            $this->dir = $this->options['dir'];
            if (!is_dir($this->dir)) {
                $this->log("$this->dir is not a directory. Exiting.");
                $this->finish(1);
            }
        }
        foreach (self::$available_options as $cli_option => $option) {
            if (isset($this->options[$cli_option])) {
                $this->fsck_options[] = $option;
            }
        }
    }
    public function run() {
        if (empty($this->dir)) {
            $task_ids = schedule_fsck_all_shares($this->fsck_options);
            $this->dir = 'all shares';
            $fsck_options = NULL;
        } else {
            $query = "INSERT INTO tasks SET action = 'fsck', share = :full_path, additional_info = :fsck_options, complete = 'yes'";
            if (empty($this->fsck_options)) {
                $fsck_options = NULL;
            } else {
                $fsck_options = implode('|', $this->fsck_options);
            }
            $params = array(
                'full_path' => $this->dir,
                'fsck_options' => $fsck_options,
            );
            $task_id = DB::insert($query, $params);
            $task_ids = array($task_id);
        }
        FSCKWorkLog::initiate($this->dir, $fsck_options, $task_ids);
        $this->log("fsck of $this->dir has been scheduled. It will start after all currently pending tasks have been completed.");
        if (isset($this->options['checksums'])) {
            $this->log("Any mismatch in checksums will be logged in both " . Config::get(CONFIG_GREYHOLE_LOG_FILE) . " and " . FSCKLogFile::PATH . "/fsck_checksums.log");
        }
        DB::deleteExecutedTasks();
    }
    protected function log($what='') {
        if (isset($_POST)) {
            error_log($what);
        } else {
            echo "$what\n";
        }
    }
    protected function logn($what) {
        if (isset($_POST)) {
            error_log($what);
        } else {
            echo $what;
        }
    }
}

class GetGUIDCliRunner extends AbstractAnonymousCliRunner {
    public function run() {
        $this->logn(GetGUIDCliRunner::setUniqID());
    }
    public static function setUniqID() {
        $uniq_id = Settings::get('uniq_id');
        if (!$uniq_id) {
            foreach (Config::storagePoolDrives() as $sp_drive) {
                $f = "$sp_drive/.greyhole_uses_this";
                if (file_exists($f) && filesize($f) == 23) {
                    $uniq_id = file_get_contents($f);
                    break;
                }
            }
            if (!$uniq_id) {
                $uniq_id = uniqid('', TRUE);
            }
            Settings::set('uniq_id', $uniq_id);
        }
        return $uniq_id;
    }
}

abstract class AbstractPoolDriveCliRunner extends AbstractCliRunner {
    protected $drive;
    function __construct($options, $cli_command) {
        parent::__construct($options, $cli_command);
        $this->assertPoolDriveSpecified();
    }
    protected function assertPoolDriveSpecified() {
        $simple_long_opt = str_replace(':', '', $this->cli_command->getLongOpt());
        $this->drive = $this->parseCmdParamAsDriveAndExpect(Config::storagePoolDrives());
        if ($this->drive === FALSE) {
            $this->log("Please use one of the following with the --$simple_long_opt option:");
            $this->log("  " . implode("\n  ", Config::storagePoolDrives()));
            $this->log("Note that the correct syntax for this command is:");
            $this->log("  greyhole --$simple_long_opt=<drive>");
            $this->log("The '=' character is mandatory.");
            $this->finish(1);
        }
    }
}

class GoneCliRunner extends AbstractPoolDriveCliRunner {
    public function run() {
        error_log("This command is deprecated. Using 'greyhole --remove' instead.");
        exit(1);
    }
}

class GoingCliRunner extends GoneCliRunner {
}

class IoStatsCliRunner extends AbstractAnonymousCliRunner {
    public function run() {
        $devices_drives = array();
        foreach (Config::storagePoolDrives() as $sp_drive) {
            $device = exec("df " . escapeshellarg($sp_drive) . " 2>/dev/null | awk '{print \$1}'");
            $device = preg_replace('@/dev/(sd[a-z])[0-9]?@', '\1', $device);
            $devices_drives[$device] = $sp_drive;
        }
        while (TRUE) {
            unset($result);
            exec("iostat -p ALL -k 5 2 | grep '^sd[a-z] ' | awk '{print \$1,\$3,\$4}'", $result);
            $iostat = array();
            foreach ($result as $line) {
                $info = explode(' ', $line);
                $device = $info[0];
                $read_kBps = $info[1];
                $write_kBps = $info[2];
                if (!isset($devices_drives[$device])) {
                    continue;
                }
                $drive = $devices_drives[$device];
                $iostat[$drive] = (int) round((int) $read_kBps + (int) $write_kBps);
            }
            ksort($iostat);
            $this->log("--- [" . date('H:m:s') . "]");
            foreach ($iostat as $drive => $io_kBps) {
                $this->log(sprintf("$drive: %7s kBps", $io_kBps));
            }
        }
    }
}

class LogsCliRunner extends AbstractAnonymousCliRunner {
    public function run() {
        $greyhole_log_file = Config::get(CONFIG_GREYHOLE_LOG_FILE);
        if (strtolower($greyhole_log_file) == 'syslog') {
            if (gh_is_file('/var/log/syslog')) {
                passthru("tail -F -n 1 /var/log/syslog | grep --line-buffered Greyhole");
            } else {
                passthru("tail -F -n 1 /var/log/messages | grep --line-buffered Greyhole");
            }
        } else {
            $files = escapeshellarg($greyhole_log_file);
            passthru("tail -n 1 $files");
            passthru("tail -qF -n 0 $files");
        }
    }
}

class MoveCliRunner extends AbstractCliRunner {
    public function run() {
        Log::setAction(ACTION_INITIALIZE);
        Metastores::choose_metastores_backups();
        Log::setAction(ACTION_MOVE);
        $argc = $GLOBALS['argc'];
        $argv = $GLOBALS['argv'];
        if ($argc != 4) {
            echo "Usage: greyhole --mv source target\n  `source` and `target` should start with a share name, following by the full path to a file or folder.\n\nExample:\n  greyhole --mv Videos/TV/24 VideosAttic/TV/\n";
            exit(1);
        }
        $source = $argv[2];
        $destination = $argv[3];
        foreach (SharesConfig::getShares() as $share_name => $share_options) {
            if ($share_name == first(explode('/', $source))) {
                $source_share = $share_name;
            }
            if ($share_name == first(explode('/', $destination))) {
                $destination_share = $share_name;
            }
        }
        if (!isset($source_share)) {
            echo "Error: source ($source) doesn't start with a Greyhole share name.\n";
            exit(2);
        }
        if (!isset($destination_share)) {
            echo "Error: destination ($destination) doesn't start with a Greyhole share name.\n";
            exit(2);
        }
        if ($source_share == $destination_share) {
            echo "Error: source and destination are on the same Greyhole share.\n";
            exit(2);
        }
        $paths = explode('/', $source);
        array_shift($paths); // Remove share name
        $full_path = implode('/', $paths);
        $source_landing_zone = get_share_landing_zone($source_share);
        $source_is_file = is_file("$source_landing_zone/$full_path");
        if (!$source_is_file) {
            $source_is_dir = is_dir("$source_landing_zone/$full_path");
            if (!$source_is_dir) {
                echo "Error: source does not exist.\n";
                exit(2);
            }
            chdir("$source_landing_zone/$full_path");
        }
        $paths = explode('/', $destination);
        array_shift($paths); // Remove share name
        $full_path = implode('/', $paths);
        $landing_zone = get_share_landing_zone($destination_share);
        $destination_folder_exists = is_dir("$landing_zone/$full_path");
        if ($source_is_file) {
            if ($destination_folder_exists) {
                $destination = clean_dir("$destination/" . basename($source));
            } else {
            }
            static::move_file($source, $destination);
            exit(0);
        } else {
            $source = trim($source, '/');
            if ($destination_folder_exists) {
                $destination = clean_dir("$destination/" . basename($source));
            } else {
            }
            exec("find . -type f -o -type l", $all_files);
            foreach ($all_files as $file) {
                $source_file = clean_dir($source . '/' . substr($file, 2));
                $destination_file = clean_dir($destination . '/' . substr($file, 2));
                static::move_file($source_file, $destination_file);
            }
            if ($source_is_dir) {
                $folder_to_delete = getcwd();
                exec("find " . escapeshellarg($folder_to_delete) . " -type d", $folders_to_delete);
                foreach ($folders_to_delete as $dir) {
                    if ($dir === $folder_to_delete) continue; // Will be deleted last
                    static::rmdir($source_share, $source_landing_zone, $dir);
                }
                static::rmdir($source_share, $source_landing_zone, $folder_to_delete);
            }
        }
    }
    protected static function rmdir($source_share, $source_landing_zone, $full_path_in_lz) {
        $full_path = trim(str_replace($source_landing_zone, '', $full_path_in_lz), '/');
        echo "[INFO] Deleting empty folder: $source_share/$full_path\n";
        rmdir("$source_landing_zone/$full_path");
        $task = AbstractTask::instantiate([
            'action'    => 'rmdir',
            'share'     => $source_share,
            'full_path' => $full_path,
        ]);
        $task->execute();
    }
    protected static function move_file($source, $destination) {
        echo "[INFO] Moving file: $source > $destination\n";
        $source_parts = explode('/', $source);
        $source_share = array_shift($source_parts);
        $source_full_path = implode('/', $source_parts);
        $source_landing_zone = get_share_landing_zone($source_share);
        $destination_parts = explode('/', $destination);
        $destination_share = array_shift($destination_parts);
        $destination_full_path = implode('/', $destination_parts);
        $sp_drives_affected = array();
        foreach (Config::storagePoolDrives() as $sp_drive) {
            if (file_exists("$sp_drive/$source")) {
                static::move_real_file("$sp_drive/$source", "$sp_drive/$destination");
                $sp_drives_affected[] = $sp_drive;
            }
        }
        if (is_link("$source_landing_zone/$source_full_path")) {
            unlink("$source_landing_zone/$source_full_path");
        }
        list($path, $filename) = explode_full_path($source_full_path);
        $metafiles = Metastores::get_metafile_data_filenames($source_share, $path, $filename);
        foreach ($metafiles as $metafile) {
            unlink($metafile);
        }
        foreach ($metafiles as $metafile) {
            @rmdir(dirname($metafile));
        }
        $fsck_task = new FsckTask(array('additional_info' => OPTION_ORPHANED));
        foreach ($sp_drives_affected as $sp_drive) {
            $full_path = "$sp_drive/$destination_share/$destination_full_path";
            list($path, $filename) = explode_full_path($full_path);
            $fsck_task->initialize_fsck_report($full_path);
            $fsck_task->gh_fsck_file($path, $filename, 'file', 'mv', $destination_share, $sp_drive);
            break;
        }
    }
    protected static function move_real_file($source, $destination) {
        $target_folder = dirname($destination);
        if (!file_exists($target_folder)) {
            $source_folder = dirname($source);
            gh_mkdir($target_folder, $source_folder);
        }
        if (file_exists($destination)) {
            echo "[WARN] $destination already exists. Will rename to $destination.bak before continuing.\n";
            rename($destination, "$destination.bak");
        }
        rename($source, $destination);
    }
}

class MD5WorkerCliRunner extends AbstractCliRunner {
    private $drives;
    function __construct($options, $cli_command) {
        $pid = pcntl_fork();
        if ($pid == -1) {
            $this->log("Error spawning child md5-worker!");
            $this->finish(1);
        }
        if ($pid == 0) {
            parent::__construct($options, $cli_command);
            if (is_array($this->options['drive'])) {
                $this->drives = $this->options['drive'];
            } else {
                $this->drives = array($this->options['drive']);
            }
        } else {
            echo $pid;
            $this->finish(0);
        }
    }
    public function run() {
        Log::setAction(ACTION_MD5_WORKER);
        $drives_clause = array();
        $params = array();
        $i = 1;
        foreach ($this->drives as $drive) {
            $key = 'drive' . ($i++);
            $drives_clause[] = "additional_info LIKE :$key";
            $params[$key] = "$drive%";
        }
        $query = "SELECT id, share, full_path, additional_info FROM tasks WHERE action = 'md5' AND complete = 'no' AND (" . implode(' OR ', $drives_clause) . ") ORDER BY id LIMIT 100";
        $last_check_time = time();
        while (TRUE) {
            $task = FALSE;
            if (!empty($new_tasks)) {
                $task = array_shift($new_tasks);
            }
            if ($task === FALSE) {
                $new_tasks = DB::getAll($query, $params);
                if (!empty($new_tasks)) {
                    $task = array_shift($new_tasks);
                }
            }
            if ($task === FALSE) {
                if (time() - $last_check_time > 3600) {
                    Log::debug("MD5 worker thread for " . implode(', ', $this->drives) . " will now exit; it has nothing more to do.");
                    break;
                }
                sleep(5);
                continue;
            }
            $last_check_time = time();
            Log::info("Working on MD5 task ID $task->id: $task->additional_info");
            $md5 = md5_file($task->additional_info);
            Log::debug("  MD5 for $task->additional_info = $md5");
            $update_query = "UPDATE tasks SET complete = 'yes', additional_info = :additional_info WHERE id = :task_id";
            $params1 = array(
                'task_id' => $task->id,
                'additional_info' => "$task->additional_info=$md5"
            );
            DB::execute($update_query, $params1);
        }
    }
}

class PauseCliRunner extends AbstractCliRunner {
    public static function isPaused() {
        $flags = exec('ps ax -o pid,stat,comm,args | grep "greyhole --daemon\|greyhole -D" | grep -v grep | grep -v bash | awk \'{print $2}\'');
        return string_contains($flags, 'T');
    }
    public function run() {
        $pid = (int) exec('ps ax -o pid,stat,comm,args | grep "greyhole --daemon\|greyhole -D" | grep -v grep | grep -v bash | awk \'{print $1}\'');
        if ($pid) {
            if ($this instanceof ResumeCliRunner) {
                exec('kill -CONT ' . $pid);
                $this->log("The Greyhole daemon (PID $pid) has resumed.");
            } else {
                exec('kill -STOP ' . $pid);
                $this->log("The Greyhole daemon (PID $pid) has been paused. Use `greyhole --resume` to restart it.");
            }
            if (isset($_POST)) {
                return TRUE;
            }
            exit(0);
        } else {
            $this->log("Couldn't find a Greyhole daemon running.");
            if (isset($_POST)) {
                return FALSE;
            }
            exit(1);
        }
    }
    protected function log($what='') {
        if (isset($_POST)) {
            error_log($what);
        } else {
            echo "$what\n";
        }
    }
    protected function logn($what) {
        if (isset($_POST)) {
            error_log($what);
        } else {
            echo $what;
        }
    }
}

class PrintFsckCliRunner extends AbstractCliRunner {
    public function run() {
        $report_body = FSCKWorkLog::getHumanReadableReport();
        echo "\n$report_body";
    }
}

class ProcessSpoolCliRunner extends AbstractCliRunner {
    public function run() {
        global $argv;
        $start = time();
        Log::setAction(ACTION_INITIALIZE);
        Metastores::choose_metastores_backups();
        Log::cleanStatusTable();
        while (time() - $start < 55) {
            SambaSpool::parse_samba_spool();
            if (!array_contains($argv, '--keepalive')) {
                return;
            }
            if (time() - $start >= 55) {
                break;
            }
            sleep(5);
        }
    }
}

class RemoveShareCliRunner extends AbstractCliRunner {
    private $share;
    function __construct($options, $cli_command) {
        parent::__construct($options, $cli_command);
        if (!isset($this->options['cmd_param'])) {
            $this->log("Please specify the share to remove.");
            $this->finish(1);
        }
        $this->share = $this->options['cmd_param'];
        if (!SharesConfig::exists($this->share)) {
            $this->log("'$this->share' is not a known share.");
            $this->log("If you removed it already from your Greyhole configuration, please re-add it, and retry.");
            $this->log("Otherwise, please use one of the following share name:");
            $this->log("  " . implode("\n  ", array_keys(SharesConfig::getShares())));
            $this->finish(1);
        }
    }
    public function run() {
        $landing_zone = get_share_landing_zone($this->share);
        $this->log("Will remove '$this->share' share from the Greyhole storage pool, by moving all the data files inside this share to it's landing zone: $landing_zone");
        $free_space = 1024 * exec("df -k " . escapeshellarg($landing_zone) . " | tail -1 | awk '{print $4}'");
        $this->log("  Finding how much space is needed, and if you have enough... Please wait...");
        $space_needed = 1024 * exec("du -skL " . escapeshellarg($landing_zone) . " | awk '{print $1}'");
        $this->log("    Space needed: " . bytes_to_human($space_needed, FALSE));
        $this->log("    Free space: " . bytes_to_human($free_space, FALSE));
        foreach (Config::storagePoolDrives() as $sp_drive) {
            if (!file_exists("$sp_drive/$this->share")) {
                continue;
            }
            if (SystemHelper::directory_uuid("$sp_drive/$this->share") === SystemHelper::directory_uuid("$landing_zone")) {
                $more_free_space = 1024 * exec("du -sk " . escapeshellarg("$sp_drive/$this->share") . " | awk '{print $1}'");
                $free_space += $more_free_space;
                $this->log("      + " . bytes_to_human($more_free_space, FALSE) . " (file copies already on $sp_drive) = " . bytes_to_human($free_space, FALSE) . " (Total available space)");
                break;
            }
        }
        if ($space_needed > $free_space) {
            $this->log("Not enough free space available in $landing_zone. Aborting.");
            $this->finish(1);
        }
        $this->log("OK! Let's do this.");
        $query = "INSERT INTO tasks SET action = :action, share = :share, complete = 'yes'";
        $params = array(
            'action'    => ACTION_REMOVE_SHARE,
            'share'     => $this->share,
        );
        DB::insert($query, $params);
        $this->log();
        $this->log("Removal of share '$this->share' has been scheduled. It will start after all currently pending tasks have been completed.");
        $this->log("You will receive an email notification once it completes.");
        $this->log("You can also tail the Greyhole log to follow this operation.");
    }
}

class RemoveCliRunner extends AbstractPoolDriveCliRunner {
    public function run() {
        echo "\nIs the specified drive still available? ";
        if (is_dir("$this->drive/.gh_metastore/")) {
            echo "(It looks like it is.)\n";
        } else {
            echo "(It looks like it is not.)\n";
        }
        echo "If so, Greyhole will try to move all file copies that are only on this drive, onto your other drives.\n";
        do {
            $answer = strtolower(readline("Yes, or No? "));
        } while ($answer != 'yes' && $answer != 'no');
        $drive_still_available = ( $answer == 'yes' );
        if ($drive_still_available) {
            $total = DB::getFirstValue("SELECT COUNT(*) AS total FROM tasks WHERE action = 'fsck'");
            if ($total > 0) {
                $this->log("There are pending fsck operations. This could mean some file copies are missing, which would make it dangerous to remove a drive at this time.");
                $this->log("Please wait until all fsck operation are complete, and then retry.");
                $this->finish(2);
            }
        }
        $query = "INSERT INTO tasks SET action = :action, share = :share, full_path = :full_path, additional_info = :options, complete = 'yes'";
        $params = array(
            'action'    => ACTION_REMOVE,
            'share'     => 'pool drive ',
            'full_path' => $this->drive,
            'options'   => ( $drive_still_available ? OPTION_DRIVE_IS_AVAILABLE : '' ),
        );
        DB::insert($query, $params);
        $this->log();
        $this->log("Removal of $this->drive has been scheduled. It will start after all currently pending tasks have been completed.");
        $this->log("You will receive an email notification once it completes.");
        $this->log("You can also tail the Greyhole log to follow this operation.");
    }
}

class ResumeCliRunner extends PauseCliRunner {
}

class ReplaceCliRunner extends AbstractPoolDriveCliRunner {
    public function run() {
        StoragePool::remove_drive($this->drive);
        Log::info("Storage pool drive $this->drive has been marked replaced. The Greyhole daemon will now be restarted to allow it to use this new drive.");
        $this->log("Storage pool drive $this->drive has been marked replaced. The Greyhole daemon will now be restarted to allow it to use this new drive.");
        $this->restart_service();
    }
}

class StatsCliRunner extends AbstractAnonymousCliRunner {
    public function run() {
        if (file_exists('/sbin/zpool')) {
            if (exec("whoami") != 'root') {
                $this->log("Warning: If you are using ZFS datasets as Greyhole storage pool drives, you will need to execute this as root.");
            }
        }
        $max_drive_strlen = max(array_map('mb_strlen', Config::storagePoolDrives())) + 1;
        $stats = static::get_stats();
        if (isset($this->options['json'])) {
            echo json_encode($stats);
        } else {
            $this->log();
            $this->log("Greyhole Statistics");
            $this->log("===================");
            $this->log();
            $this->log("Storage Pool");
            $this->log(sprintf("%$max_drive_strlen"."s    Total -   Used =   Free +  Trash = Possible", ''));
            foreach ($stats as $sp_drive => $stat) {
                if ($sp_drive == 'Total') {
                    $this->log(sprintf("  %-$max_drive_strlen"."s ==========================================", ""));
                }
                $this->logn(sprintf("  %-$max_drive_strlen"."s ", "$sp_drive:"));
                if (empty($stat->total_space)) {
                    $this->log("                 Offline                  ");
                } else {
                    $this->log(
                            sprintf('%5.0f', $stat->total_space/1024/1024) . "G"               //   Total
                        . ' - ' . sprintf('%5.0f', $stat->used_space/1024/1024). "G"                 // - Used
                        . ' = ' . sprintf('%5.0f', $stat->free_space/1024/1024) . "G"                // = Free
                        . ' + ' . sprintf('%5.0f', $stat->trash_size/1024/1024) . "G"                // + Trash
                        . ' = ' . sprintf('%5.0f', $stat->potential_available_space/1024/1024) . "G" // = Possible
                    );
                }
            }
            $this->log();
        }
    }
    public static function get_stats() {
        $totals = array(
            'total_space' => 0,
            'used_space' => 0,
            'free_space' => 0,
            'trash_size' => 0,
            'potential_available_space' => 0
        );
        $stats = array();
        foreach (Config::storagePoolDrives() as $sp_drive) {
            $df = StoragePool::get_free_space($sp_drive);
            if (!$df || !file_exists($sp_drive)) {
                $stats[$sp_drive] = (object) array();
                continue;
            }
            if (!StoragePool::is_pool_drive($sp_drive)) {
                $stats[$sp_drive] = (object) array();
                continue;
            }
            $df_command = "df -k " . escapeshellarg($sp_drive) . " | tail -1";
            unset($responses);
            exec($df_command, $responses);
            $total_space = 0;
            $used_space = 0;
            if (isset($responses[0])) {
                if (preg_match("@\s+([0-9]+)\s+([0-9]+)\s+[0-9]+\s+[0-9]+%\s+.+$@", $responses[0], $regs)) {
                    $total_space = (float) $regs[1];
                    $used_space = (float) $regs[2];
                }
            }
            $free_space = $df['free'];
            $trash_path = clean_dir("$sp_drive/.gh_trash");
            if (!file_exists($trash_path)) {
                $trash_size = (float) 0;
            } else {
                $trash_size = (float) trim(exec("du -sk " . escapeshellarg($trash_path) . " | awk '{print $1}'"));
            }
            $potential_available_space = (float) $free_space + $trash_size;
            $stats[$sp_drive] = (object) array(
                'total_space' => $total_space,
                'used_space' => $used_space,
                'free_space' => $free_space,
                'trash_size' => $trash_size,
                'potential_available_space' => $potential_available_space,
            );
            $totals['total_space'] += $total_space;
            $totals['used_space'] += $used_space;
            $totals['free_space'] += $free_space;
            $totals['trash_size'] += $trash_size;
            $totals['potential_available_space'] += $potential_available_space;
        }
        $stats['Total'] = (object) $totals;
        return $stats;
    }
}

class StatusCliRunner extends AbstractAnonymousCliRunner {
    public function run() {
        $num_dproc = static::get_num_daemon_proc();
        if ($num_dproc == 0) {
            $this->log();
            $this->log("Greyhole daemon is currently stopped.");
            $this->log();
            $this->finish(1);
        }
        if (PauseCliRunner::isPaused()) {
            $this->log();
            $this->log("Greyhole daemon is currently paused. Use `greyhole --resume` to restart it.");
            $this->log();
            $this->finish(1);
        }
        $tasks = DBSpool::getInstance()->fetch_next_tasks(TRUE, FALSE);
        if (empty($tasks)) {
            $this->log();
            $this->log("Currently idle.");
        } else {
            $task = array_shift($tasks);
            $this->log();
            $this->log("Currently working on task ID $task->id: $task->action " . clean_dir("$task->share/$task->full_path") . ($task->action == 'rename' ? " -> " . clean_dir("$task->share/$task->additional_info") : ''));
        }
        $this->log();
        $this->log("Recent log entries:");
        foreach (static::get_recent_status_entries() as $log) {
            $date = date("M d H:i:s", strtotime($log->date_time));
            $log_text = sprintf("%s%s",
                "$date $log->action: ",
                $log->log
            );
            $this->log("  $log_text");
        }
        list($last_action, $last_action_time) = static::get_last_action();
        $this->log();
        $this->log("Last logged action: $last_action");
        if (!empty($last_action_time)) {
            $this->log("  on " . date('Y-m-d H:i:s', $last_action_time) . " (" . how_long_ago($last_action_time) . ")");
        }
        $this->log();
    }
    public static function get_recent_status_entries() {
        $q = "SELECT * FROM status ORDER BY id DESC LIMIT 15";
        return array_reverse(DB::getAll($q));
    }
    public static function get_num_daemon_proc() {
        return (int) exec('ps ax | grep "greyhole --daemon\|greyhole -D" | grep -v grep | grep -v bash | wc -l');
    }
    public static function get_last_action() {
        $last_log_line = exec("tail -n 1 " . escapeshellarg(Config::get(CONFIG_GREYHOLE_LOG_FILE)) . " 2>/dev/null");
        if (empty($last_log_line)) {
            return ['N/A', NULL];
        }
        $last_action_time = strtotime(mb_substr($last_log_line, 0, 15));
        $raw_last_log_line = mb_substr($last_log_line, 16);
        $last_log_line = explode(' ', $raw_last_log_line);
        $last_action = str_replace(':', '', $last_log_line[1]);
        return [$last_action, $last_action_time];
    }
}

class TestCliRunner extends AbstractCliRunner {
    public function run() {
        ConfigHelper::test();
        DB::connect();
        DB::repairTables();
        MigrationHelper::convertStoragePoolDrivesTagFiles();
        echo "Config is OK\n";
        exit(0);
    }
}

class ThawCliRunner extends AbstractCliRunner {
    private $dir;
    function __construct($options, $cli_command) {
        parent::__construct($options, $cli_command);
        $frozen_directories = Config::get(CONFIG_FROZEN_DIRECTORY);
        $this->dir = $this->parseCmdParamAsDriveAndExpect($frozen_directories);
        if ($this->dir === FALSE) {
            $this->printUsage();
        }
    }
    private function printUsage() {
        $this->log("Frozen directories:");
        $this->log("  " . implode("\n  ", Config::get(CONFIG_FROZEN_DIRECTORY)));
        $this->log("To thaw any of the above directories, use the following command:");
        $this->log("  greyhole --thaw=directory");
        $this->finish(0);
    }
    public function run() {
        $path = explode('/', $this->dir);
        $share = array_shift($path);
        $query = "UPDATE tasks SET complete = 'thawed' WHERE complete = 'frozen' AND share = :share AND full_path LIKE :path";
        DB::execute($query, array('share' => $share, 'path' => implode('/', $path).'%'));
        $this->log("$this->dir directory has been thawed.");
        $this->log("All pasts file operations that occured in this directory will now be processed by Greyhole.");
    }
}

class ViewQueueCliRunner extends AbstractAnonymousCliRunner {
    public function run() {
        $shares_names = array_keys(SharesConfig::getShares());
        natcasesort($shares_names);
        $max_share_strlen = max(array_merge(array_map('mb_strlen', $shares_names), array(7)));
        $queues = static::getData();
        if (isset($this->options['json'])) {
            echo json_encode($queues);
        } else {
            $this->log();
            $this->log("Greyhole Work Queue Statistics");
            $this->log("==============================");
            $this->log();
            $this->log("This table gives you the number of pending operations queued for the Greyhole daemon, per share.");
            $this->log();
            $col_size = 7;
            foreach ($queues['Total'] as $num) {
                $num = number_format($num, 0);
                if (strlen($num) > $col_size) {
                    $col_size = strlen($num);
                }
            }
            $col_format = '%' . $col_size . 's';
            $header = sprintf("%$max_share_strlen"."s  $col_format  $col_format  $col_format  $col_format", '', 'Write', 'Delete', 'Rename', 'Check');
            $this->log($header);
            foreach ($queues as $share_name => $queue) {
                if ($share_name == 'Spooled') continue;
                if ($share_name == 'Total') {
                    $this->log(str_repeat('=', $max_share_strlen+2+(4*$col_size)+(3*2)));
                }
                $this->log(sprintf("%-$max_share_strlen"."s", $share_name) . "  "
                    . sprintf($col_format, number_format($queue->num_writes_pending, 0)) . "  "
                    . sprintf($col_format, number_format($queue->num_delete_pending, 0)) . "  "
                    . sprintf($col_format, number_format($queue->num_rename_pending, 0)) . "  "
                    . sprintf($col_format, number_format($queue->num_fsck_pending, 0))
                );
            }
            $this->log($header);
            $this->log();
            $this->log("The following is the number of pending operations that the Greyhole daemon still needs to parse.");
            $this->log("Until it does, the nature of those operations is unknown.");
            $this->log("Spooled operations that have been parsed will be listed above and disappear from the count below.");
            $this->log();
            $this->log(sprintf("%-$max_share_strlen"."s  ", 'Spooled') . number_format($queues['Spooled'], 0));
            $this->log();
        }
    }
    public static function getData() {
        $shares_names = array_keys(SharesConfig::getShares());
        natcasesort($shares_names);
        $queues = array();
        $total_num_writes_pending = $total_num_delete_pending = $total_num_rename_pending = $total_num_fsck_pending = 0;
        foreach ($shares_names as $share_name) {
            $num_writes_pending = (int) DB::getFirstValue("SELECT COUNT(*) FROM tasks WHERE action = 'write' AND share = :share AND complete IN ('yes', 'thawed', 'written')", array('share' => $share_name));
            $total_num_writes_pending += $num_writes_pending;
            $num_delete_pending = (int) DB::getFirstValue("SELECT COUNT(*) FROM tasks WHERE (action = 'unlink' OR action = 'rmdir') AND share = :share AND complete IN ('yes', 'thawed', 'written')", array('share' => $share_name));
            $total_num_delete_pending += $num_delete_pending;
            $num_rename_pending = (int) DB::getFirstValue("SELECT COUNT(*) FROM tasks WHERE action = 'rename' AND share = :share AND complete IN ('yes', 'thawed', 'written')", array('share' => $share_name));
            $total_num_rename_pending += $num_rename_pending;
            $num_fsck_pending = (int) DB::getFirstValue("SELECT COUNT(*) FROM tasks WHERE (action = 'fsck' OR action = 'fsck_file' OR action = 'md5') AND share = :share", array('share' => $share_name));
            $landing_zone = SharesConfig::get($share_name, CONFIG_LANDING_ZONE);
            $num_fsck_pending += (int) DB::getFirstValue("SELECT COUNT(*) FROM tasks WHERE (action = 'fsck' OR action = 'fsck_file' OR action = 'md5') AND share LIKE :landing_zone", array('landing_zone' => "$landing_zone/%"));
            $total_num_fsck_pending += $num_fsck_pending;
            $queues[$share_name] = (object) array(
                'num_writes_pending' => $num_writes_pending,
                'num_delete_pending' => $num_delete_pending,
                'num_rename_pending' => $num_rename_pending,
                'num_fsck_pending' => $num_fsck_pending,
            );
        }
        $queues['Total'] = (object) array(
            'num_writes_pending' => $total_num_writes_pending,
            'num_delete_pending' => $total_num_delete_pending,
            'num_rename_pending' => $total_num_rename_pending,
            'num_fsck_pending' => $total_num_fsck_pending,
        );
        $queues['Spooled'] = (int) exec("find -L /var/spool/greyhole -type f 2> /dev/null | wc -l");
        return $queues;
    }
}

class WaitForCliRunner extends AbstractPoolDriveCliRunner {
    public function run() {
        StoragePool::mark_gone_ok($this->drive);
        Log::info("Storage pool drive $this->drive has been marked Temporarily-Gone");
        $this->log("Storage pool drive $this->drive has been marked Temporarily-Gone, which means the missing file copies that are in this drive will not be re-created until it reappears.");
    }
}

class CommandLineHelper {
    protected $actionCmd = null;
    protected $options = array();
    protected $cliCommandsDefinitions;
    protected $cliOptionsDefinitions;
    function __construct() {
        $this->cliCommandsDefinitions = array(
            new CliCommandDefinition('help',             '?',   null,             null,                           "Display this help and exit."),
            new CliCommandDefinition('daemon',           'D',   null,             DaemonRunner::class,            "Start the daemon."),
            new CliCommandDefinition('pause',            'P',   null,             PauseCliRunner::class,          "Pause the daemon."),
            new CliCommandDefinition('resume',           'M',   null,             ResumeCliRunner::class,         "Resume a paused daemon."),
            new CliCommandDefinition('fsck',             'f',   null,             FsckCliRunner::class,           "Schedule a fsck."),
            new CliCommandDefinition('cancel-fsck',      'C',   null,             CancelFsckCliRunner::class,     "Cancel any ongoing or scheduled fsck operations."),
            new CliCommandDefinition('print-fsck',       'F',   null,             PrintFsckCliRunner::class,      "Print the fsck report for the last completed fsck task. This will print the same content that is sent by email when the --email-report option is used."),
            new CliCommandDefinition('balance',          'l',   null,             BalanceCliRunner::class,        "Balance available space on storage pool drives."),
            new CliCommandDefinition('balance-status',   '',    null,             BalanceStatusCliRunner::class,  "Verify how balanced are the storage pool drives."),
            new CliCommandDefinition('cancel-balance',   'B',   null,             CancelBalanceCliRunner::class,  "Cancel any ongoing or scheduled balance operations."),
            new CliCommandDefinition('stats',            's',   null,             StatsCliRunner::class,          "Display storage pool statistics."),
            new CliCommandDefinition('iostat',           'i',   null,             IoStatsCliRunner::class,        "I/O statistics for your storage pool drives."),
            new CliCommandDefinition('logs',             'L',   null,             LogsCliRunner::class,           "Display new greyhole.log entries as they are logged."),
            new CliCommandDefinition('status',           'S',   null,             StatusCliRunner::class,         "Display what the Greyhole daemon is currently doing."),
            new CliCommandDefinition('view-queue',       'q',   null,             ViewQueueCliRunner::class,      "Display the current work queue."),
            new CliCommandDefinition('empty-trash',      'a',   null,             EmptyTrashCliRunner::class,     "Empty the trash."),
            new CliCommandDefinition('mv',               '',    ' source target', MoveCliRunner::class,           "Move a folder or file from one Greyhole share to another. Run without parameters for details."),
            new CliCommandDefinition('cp',               '',    ' source share/target/dir/', CopyCliRunner::class,"Copy a file or folder onto your storage pool without going through Samba. Run without parameters for details."),
            new CliCommandDefinition('debug:',           'b:',  '=filename',      DebugCliRunner::class,          "Debug past file operations."),
            new CliCommandDefinition('thaw::',           't::', '[=path]',        ThawCliRunner::class,           "Thaw a frozen directory. Greyhole will start working on files inside <path>. If you don't supply an option, the list of frozen directories will be displayed."),
            new CliCommandDefinition('wait-for::',       'w::', '[=path]',        WaitForCliRunner::class,        "Tell Greyhole that the missing drive at <path> will return soon, and that it shouldn't re-create additional file copies to replace it. If you don't supply an option, the available options (paths) will be displayed."),
            new CliCommandDefinition('gone::',           '',    null,             GoneCliRunner::class,           null),
            new CliCommandDefinition('going::',          '',    null,             GoingCliRunner::class,          null),
            new CliCommandDefinition('remove::',         'R::', '[=path]',        RemoveCliRunner::class,         "Tell Greyhole that you want to remove a drive. Greyhole will then make sure you don't lose any files, and that the correct number of file copies are created to replace the missing drive. If you don't supply an option, the available options (paths) will be displayed."),
            new CliCommandDefinition('replaced::',       'r::', '[=path]',        ReplaceCliRunner::class,        "Tell Greyhole that you replaced the drive at <path>."),
            new CliCommandDefinition('fix-symlinks',     'X',   null,             FixSymlinksCliRunner::class,    "Try to find a good file copy to point to for all broken symlinks found on your shares."),
            new CliCommandDefinition('delete-metadata:', 'p:',  '=path',          DeleteMetadataCliRunner::class, "Delete all metadata files for <path>, which should be a share name, followed by the path to a file that is gone from your storage pool. Eg. 'Movies/HD/The Big Lebowski.mkv'" ),
            new CliCommandDefinition('remove-share:',    'U:',  '=share_name',    RemoveShareCliRunner::class,    "Move the files currently inside the specified share from the storage pool into the shared folder (landing zone), effectively removing the share from Greyhole's storage pool."),
            new CliCommandDefinition('config',           '',    ' name',          ConfigCliRunner::class,         "Get a config from greyhole.conf; outputs JSON"),
            new CliCommandDefinition('config',           '',    ' name value',    ConfigCliRunner::class,         "Change a config in greyhole.conf"),
            new CliCommandDefinition('md5-worker',       '',    null,             null,                           null),
            new CliCommandDefinition('getuid',           'G',   null,             GetGUIDCliRunner::class,        null),
            new CliCommandDefinition('create-mem-spool', '',    null,             CreateMemSpoolRunner::class,    null),
            new CliCommandDefinition('test-config',      '',    null,             TestCliRunner::class,           null),
            new CliCommandDefinition('boot-init',        '',    null,             BootInitCliRunner::class,       null),
            new CliCommandDefinition('process-spool',    '',    null,             ProcessSpoolCliRunner::class,   null),
        );
        $this->cliOptionsDefinitions = array(
            'json'                     => new CliOptionDefinition('json',                     'j',  null,    "Output the result as JSON, instead of human-readable text."),
            'email-report'             => new CliOptionDefinition('email-report',             'e',  null,    "Send an email when fsck completes, to report on what was checked, and any error that was found."),
            'dont-walk-metadata-store' => new CliOptionDefinition('dont-walk-metadata-store', 'y',  null,    "Speed up fsck by skipping the scan of the metadata store directories. Scanning the metadata stores is only required to re-create symbolic links that might be missing from your shared directories."),
            'if-conf-changed'          => new CliOptionDefinition('if-conf-changed',          'c',  null,    "Only fsck if greyhole.conf or smb.conf paths changed since the last fsck.\nUsed in the daily cron to prevent unneccesary fsck runs."),
            'dir'                      => new CliOptionDefinition('dir:',                     'd:', '=path', "Only scan a specific directory, and all sub-directories. The specified directory should be a Samba share, a sub-directory of a Samba share, or any directory on a storage pool drive."),
            'find-orphaned-files'      => new CliOptionDefinition('find-orphaned-files',      'o',  null,    "Scan for files with no metadata in the storage pool drives. This will allow you to include existing files on a drive in your storage pool without having to copy them manually."),
            'checksums'                => new CliOptionDefinition('checksums',                'k',  null,    "Read ALL files in your storage pool, and check that file copies are identical. This will identify any problem you might have with your file-systems.\nNOTE: this can take a LONG time to complete, since it will read everything from all your drives!"),
            'delete-orphaned-metadata' => new CliOptionDefinition('delete-orphaned-metadata', 'm',  null,    "When fsck find metadata files with no file copies, delete those metadata files. If the file copies re-appear later, you'll need to run fsck with --find-orphaned-files to have them reappear in your shares."),
            'disk-usage-report'        => new CliOptionDefinition('disk-usage-report',        'u',  null,    "Calculate the disk usage of scanned folders."),
            'drive'                    => new CliOptionDefinition('drive:',                   'R:', '=path', null),
        );
    }
    public function processCommandLine() {
        $command_line_options = $this->getopt($this->getOpts(), $this->getLongOpts());
        $this->actionCmd = $this->getActionCommand($command_line_options);
        $this->options = $this->getOptions($command_line_options);
        return $this->getRunner();
    }
    private function getActionCommand($command_line_options) {
        foreach ($this->cliCommandsDefinitions as $def) {
            $param = $def->paramSpecified($command_line_options);
            if ($param !== FALSE) {
                if ($param !== TRUE) {
                    $this->options['cmd_param'] = $param;
                }
                return $def;
            }
        }
        return null;
    }
    private function getOptions($command_line_options) {
        $options = $this->options;
        foreach ($this->cliOptionsDefinitions as $opt_name => $def) {
            $param = $def->paramSpecified($command_line_options);
            if ($param !== FALSE) {
                $options[$opt_name] = $param;
            }
        }
        return $options;
    }
    private function getRunner() {
        if (empty($this->actionCmd) && basename(first($GLOBALS['argv'], '')) == 'cpgh') {
            return new CopyCliRunner($this->options, $this->actionCmd);
        }
        if (empty($this->actionCmd) || $this->actionCmd->getOpt() == 'help') {
            $this->printUsage();
            exit(0);
        }
        if ($this->actionCmd->getLongOpt() == 'md5-worker') {
            $cliRunner = new MD5WorkerCliRunner($this->options, $this->actionCmd);
        }
        if ($this->actionCmd->getLongOpt() == 'create-mem-spool') {
            $cliRunner = new CreateMemSpoolRunner($this->options, $this->actionCmd);
        } else {
            if ($this->actionCmd->getLongOpt() != 'test-config') {
                ConfigHelper::test();
                $retry_until_successful = ( $this->actionCmd->getLongOpt() == 'boot-init' );
                if ($this->actionCmd->getLongOpt() != 'config') {
                    DB::connect($retry_until_successful);
                }
            }
            if (!isset($cliRunner)) {
                $cliRunner = $this->actionCmd->getNewRunner($this->options);
                if ($cliRunner === FALSE) {
                    $this->printUsage();
                    exit(0);
                }
            }
        }
        if (!$cliRunner->canRun()) {
            echo "You need to execute this as root.\n";
            exit(1);
        }
        Log::setAction($this->actionCmd->getLongOpt());
        return $cliRunner;
    }
    private function printUsage() {
        echo "greyhole, version 0.15.27, for linux-gnu (noarch)\n";
        echo "This software comes with ABSOLUTELY NO WARRANTY. This is free software,\n";
        echo "and you are welcome to modify and redistribute it under the GPL v3 license.\n";
        echo "\n";
        echo "Usage: greyhole [ACTION] [OPTIONS]\n";
        echo "\n";
        echo "Where ACTION is one of:\n";
        foreach ($this->cliCommandsDefinitions as $def) {
            echo $def->getUsage();
        }
        echo "\n";
        echo "For --stats and --view-queue, the available OPTIONS are:\n";
        echo $this->cliOptionsDefinitions['json']->getUsage();
        echo "\n";
        echo "For --fsck, the available OPTIONS are:\n";
        foreach ($this->cliOptionsDefinitions as $opt_name => $def) {
            if ($opt_name != 'json') {
                echo $def->getUsage();
            }
        }
    }
    private function getOpts() {
        $opts = array();
        foreach ($this->cliCommandsDefinitions as $def) {
            $opts[] = $def->getOpt();
        }
        foreach ($this->cliOptionsDefinitions as $def) {
            $opts[] = $def->getOpt();
        }
        return $opts;
    }
    private function getLongOpts() {
        $long_opts = array();
        foreach ($this->cliCommandsDefinitions as $def) {
            $long_opts[] = $def->getLongOpt();
        }
        foreach ($this->cliOptionsDefinitions as $def) {
            $long_opts[] = $def->getLongOpt();
        }
        return $long_opts;
    }
    protected function getopt($short_options, $long_options) {
        $opts_no_value = array();
        $opts_required_value = array();
        $opts_optional_value = array();
        foreach (array_merge($short_options, $long_options) as $a) {
            if (substr($a, -2) == "::" ) {
                $opts_optional_value[] = substr($a, 0, -2);
            } else if (substr($a, -1) == ":") {
                $opts_required_value[] = substr($a, 0, -1);
            } else {
                $opts_no_value[] = $a;
            }
        }
        $argv = $GLOBALS['argv'];
        $options = array();
        for ($i = 0; $i < count($argv); ) {
            $arg = $argv[$i];
            if ($arg == "-") {
                $i++;
                continue;
            }
            if ($arg[0] != "-") {
                $i++;
                continue;
            }
            if (!empty($argv[$i+1]) && $argv[$i+1][0] != "-") {
                $nextArg = $argv[$i+1];
            } else {
                $nextArg = FALSE;
            }
            if (substr($arg, 0, 2) == "--") {
                $key = substr($arg, 2);
                if (string_contains($key, '=')) {
                    list($key, $value) = explode('=', $key, 2);
                    if (array_contains($opts_required_value, $key) || array_contains($opts_optional_value, $key)) {
                        $options[$key][] = $value;
                    }
                    $i++;
                    continue;
                }
                if (array_contains($opts_required_value, $key)) {
                    $options[$key][] = $argv[$i+1];
                    $i += 2;
                    continue;
                } else if (array_contains($opts_optional_value, $key)) {
                    if ($nextArg) {
                        $options[$key][] = $nextArg;
                        $i += 2;
                    } else {
                        $options[$key][] = FALSE;
                        $i++;
                    }
                    continue;
                } else if (array_contains($opts_no_value, $key)) {
                    $options[$key][] = FALSE;
                    $i++;
                    continue;
                } else {
                    $i++;
                    continue;
                }
            } else {
                for ($j=1; $j < strlen($arg); $j++) {
                    if (array_contains($opts_no_value, $arg[$j])) {
                        $options[$arg[$j]][] = FALSE;
                        if ($j == strlen($arg) - 1) {
                            break;
                        }
                    } if (array_contains($opts_required_value, $arg[$j])) {
                        if ($j == strlen($arg) - 1) {
                            $options[$arg[$j]][] = $argv[$i+1];
                            $i++;
                        } else {
                            $options[$arg[$j]][] = substr($arg, $j+1);
                        }
                        break;
                    } else {
                        if ($j == strlen($arg) - 1 && $nextArg) {
                            $options[$arg[$j]][] = $nextArg;
                            $i++;
                        } else {
                            $options[$arg[$j]][] = FALSE;
                        }
                    }
                }
            }
            $i++;
        }
        foreach ($options as $key => $value) {
            if (count($value) == 1) {
                $options[$key] = $value[0];
            }
        }
        return $options;
    }
}

class DaemonRunner extends AbstractRunner {
	public static $was_idle = TRUE;
	private static $process_is_daemon = FALSE;
	public static function isCurrentProcessDaemon() {
		return static::$process_is_daemon;
	}
	public function run() {
		if (self::isRunning()) {
			die("Found an already running Greyhole daemon with PID " . self::getPID() . ".\nCan't start multiple Greyhole daemons.\nQuitting.\n");
		}
		static::$process_is_daemon = TRUE;
		$log = "Greyhole (version 0.15.27) daemon started.";
		Log::info($log);
		$this->initialize();
        LogHook::trigger(LogHook::EVENT_TYPE_IDLE, Log::EVENT_CODE_IDLE, $log);
        $db_spool = DBSpool::getInstance();
		while (TRUE) {
			SambaSpool::parse_samba_spool();
            StoragePool::check_drives();
			$db_spool->execute_next_task();
		}
	}
	private static function isRunning() {
        $num_daemon_processes = exec('ps ax | grep "greyhole --daemon\|greyhole -D" | grep -v grep | grep -v bash | grep -v "sudo" | wc -l');
	    return $num_daemon_processes > 1;
	}
	private static function getPID() {
        return exec('ps ax | grep "greyhole --daemon\|greyhole -D" | grep -v grep | grep -v bash | grep -v "sudo" | grep -v ' . getmypid() . ' | awk "{print \$1}"');
	}
	private function initialize() {
        DB::repairTables();
        GetGUIDCliRunner::setUniqID();
		Settings::set('last_known_config_hash', get_config_hash());
        MigrationHelper::terminologyConversion();
        Metastores::choose_metastores_backups();
        Settings::backup();
        SambaUtils::samba_check_vfs();
		SambaSpool::create_mem_spool();
		SambaSpool::parse_samba_spool();
        gh_mkdir('/var/cache/greyhole-dfree', NULL, (object) array('fileowner' => 0, 'filegroup' => 0, 'fileperms' => (int) base_convert("0777", 8, 10)));
	}
	public function finish($returnValue = 0) {
	}
	public static function restart_service() {
		if (is_file('/etc/init.d/greyhole')) {
			exec("/etc/init.d/greyhole restart");
			return TRUE;
		} else if (is_file('/etc/init/greyhole.conf')) {
			exec("/sbin/restart greyhole");
			return TRUE;
		} else if (is_file('/usr/bin/supervisorctl')) {
			exec("/usr/bin/supervisorctl status greyhole", $out, $return);
			if ($return === 0) {
				exec("/usr/bin/supervisorctl restart greyhole");
				return TRUE;
			}
		}
		return FALSE;
	}
}

$cliHelper = new CommandLineHelper();
$runner = $cliHelper->processCommandLine();
$runner->run();
$runner->finish();
?>
