includes/StoragePool.php
<?php
/*
Copyright 2009-2020 Guillaume Boudreau
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/>.
*/
final class StoragePool {
private static $greyhole_owned_drives = array();
private static $gone_ok_drives = NULL;
private static $fscked_gone_drives = NULL;
// df results cache:
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) {
// Maybe this is a remote mount? Those don't have UUIDs, so we use the .greyhole_uses_this technique.
$is_greyhole_owned_drive = file_exists("$sp_drive/.greyhole_uses_this");
if ($is_greyhole_owned_drive && isset($drives_definitions[$sp_drive])) {
// This remote drive was listed in MySQL; it shouldn't be. Let's remove it.
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);
// If last 'df' ran less than 10s ago, all the drives are already awake; no harm checking them at this time.
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])) {
// $sp_drive is now back
$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();
}
}
// Is it OK for a drive to be gone?
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 files leftovers of stopped Greyhole executions
$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)) {
// This can happen if multiple mounts exist for this drive, and the first one appearing in the output of 'df' is NOT the one for the storage pool
// In Docker, when using -v to mount a storage pool drive, and another folder in that storage pool drive:
// eg. docker run ... -v /mnt/hdd5/backups:/backups -v /mnt/hdd5:/mnt/hdd5 ...
// For this, 'df -k /mnt/hdd5' will actually return '/dev/sdX ... ... ... ...% /backups'
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];
}
}
}
/** @noinspection PhpUndefinedVariableInspection */
$dfs[$sp_drive]['free'] = $target_freespace;
/** @noinspection PhpUndefinedVariableInspection */
$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;
}
/** @var $drives_selectors PoolDriveSelector[] */
$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)) {
// All DS are empty; exit.
break;
}
}
// Email notification when all drives are over-capacity
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) {
/** @noinspection PhpUndefinedVariableInspection */
$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) {
/** @noinspection PhpUndefinedVariableInspection */
$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)) {
// Stick files into specific drives: $stick_into
// Let's check if those drives are listed in the config file!
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, '*')) {
// Contains a wildcard... In this case, we want each directory that match the wildcard to have it's own setting. Let's find this directory...
// For example, if $share_dir == 'Videos/Movies/*/*' and "$share/$path/" == "Videos/Movies/HD/La Vita e Bella/", we want to save a 'stick_into' setting for 'Videos/Movies/HD/La Vita e Bella/'
// Files in other subdirectories of Videos/Movies/HD/ could end up in other drives.
$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;
}
// Stick files into any drives
$setting_name = sprintf('stick_into-%s', $sticky_dir);
/** @noinspection PhpConditionAlreadyCheckedInspection */
$setting = Settings::get($setting_name, TRUE);
if ($setting) {
$stick_into = array_merge($stick_into, $setting);
// Let's check if those drives are listed in the config file!
$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);
}
}
// Make sure the drives we want to use are not yet full and have available space
$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;
}
}
?>