mirror of
https://github.com/unraid/webgui.git
synced 2026-05-06 12:21:34 -05:00
69a21de7a4
- Initialize outside the main loop to ensure correct state handling for move/cleanup phases - If file_manager restarts and PID is loaded from file and move operation is still active, enable to cleanup empty directories afterwards - Clarify comment for INI job parameters
375 lines
14 KiB
PHP
Executable File
375 lines
14 KiB
PHP
Executable File
#!/usr/bin/php -q
|
|
<?PHP
|
|
/* Copyright 2005-2025, Lime Technology
|
|
* Copyright 2012-2025, Bergware International.
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License version 2,
|
|
* as published by the Free Software Foundation.
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*/
|
|
?>
|
|
<?
|
|
$docroot = '/usr/local/emhttp';
|
|
$active = '/var/tmp/file.manager.active';
|
|
$pid_file = '/var/tmp/file.manager.pid';
|
|
$status = '/var/tmp/file.manager.status';
|
|
$error = '/var/tmp/file.manager.error';
|
|
$empty_dir = '/var/tmp/file.manager.empty_dir/'; // trailing slash is required for rsync
|
|
$null = '/dev/null';
|
|
$timer = time();
|
|
|
|
require_once "$docroot/webGui/include/Wrappers.php";
|
|
require_once "$docroot/webGui/include/publish.php";
|
|
extract(parse_plugin_cfg('dynamix', true));
|
|
|
|
// add translations
|
|
$_SERVER['REQUEST_URI'] = '';
|
|
$login_locale = $display['locale'] ?? '';
|
|
require_once "$docroot/webGui/include/Translations.php";
|
|
|
|
// remember current language
|
|
$locale_init = $locale;
|
|
|
|
function pool_only(&$disks) {
|
|
return array_filter($disks, function($disk){return $disk['type'] == 'Cache' && !empty($disk['uuid']);});
|
|
}
|
|
|
|
function pools_filter(&$disks) {
|
|
return array_keys(pool_only($disks));
|
|
}
|
|
|
|
function delete_file(...$file) {
|
|
array_map('unlink', array_filter($file, 'file_exists'));
|
|
}
|
|
|
|
function pgrep($pid) {
|
|
$pid = is_array($pid) ? $pid[0] : $pid;
|
|
return $pid && file_exists("/proc/$pid") ? $pid : false;
|
|
}
|
|
|
|
function isdir($name) {
|
|
return mb_substr($name, -1) == '/';
|
|
}
|
|
|
|
function truepath($name) {
|
|
$bits = array_filter(explode('/', $name), 'mb_strlen');
|
|
$path = [];
|
|
foreach ($bits as $bit) {
|
|
if ($bit == '.') continue;
|
|
if ($bit == '..') array_pop($path); else $path[] = $bit;
|
|
}
|
|
return '/'.implode('/', $path);
|
|
}
|
|
|
|
function validname($name, $real=true) {
|
|
$path = $real ? realpath(dirname($name)) : truepath(dirname($name));
|
|
$root = explode('/', $path)[1] ?? '';
|
|
return in_array($root, ['mnt','boot']) ? $path.'/'.basename($name).(mb_substr($name,-1) == '/' ? '/' : '') : '';
|
|
}
|
|
|
|
function update_translation($locale) {
|
|
global $docroot,$language;
|
|
$language = [];
|
|
if ($locale) {
|
|
$text = "$docroot/languages/$locale/translations.txt";
|
|
if (file_exists($text)) {
|
|
$store = "$docroot/languages/$locale/translations.dot";
|
|
if (!file_exists($store)) file_put_contents($store, serialize(parse_lang_file($text)));
|
|
$language = unserialize(file_get_contents($store));
|
|
}
|
|
}
|
|
}
|
|
|
|
function cat($file) {
|
|
global $null;
|
|
$cat = $set = [];
|
|
$rows = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
if (count($rows) > 0) {
|
|
natcasesort($rows);
|
|
$user = array_filter($rows, function($path){return preg_match('/^\/mnt\/user0?\//', $path);});
|
|
if (count($user) > 0) {
|
|
for ($n=0; $n < count($user); $n+=100) {
|
|
$name = array_slice($user, $n, 100);
|
|
$set[] = exec("getfattr --no-dereference --absolute-names --only-values -n system.LOCATIONS ".quoted($name)." 2>$null");
|
|
}
|
|
$disks = parse_ini_file('state/disks.ini', true);
|
|
$tag = implode('|',array_merge(['disk'],pools_filter($disks)));
|
|
$set = explode(';',str_replace(',;',',',preg_replace("/($tag)/", ';$1', implode($set))));
|
|
}
|
|
foreach (array_diff($rows, $user) as $row) {
|
|
[$none, $root, $main] = explode('/',$row,4);
|
|
$cat[] = ($root == 'mnt' ? $main : ($root == 'boot' ? 'flash' : '---'))."\0".$row;
|
|
}
|
|
$i = 0;
|
|
foreach ($user as $row) $cat[] = $set[++$i]."\0".$row;
|
|
}
|
|
return "#cat#\n".implode("\n",$cat)."\n";
|
|
}
|
|
|
|
function escape($name) {return escapeshellarg(validname($name));}
|
|
function quoted($name) {return is_array($name) ? implode(' ',array_map('escape', $name)) : escape($name);}
|
|
function source($name) {return is_array($name) ? implode(' ',array_map('escapeshellarg', $name)) : escapeshellarg($name);}
|
|
|
|
// Escape rsync filter meta characters in a single path component
|
|
function rsync_escape_component($s) {
|
|
// escape: *, ?, [, ], \
|
|
return preg_replace('/([*?\[\]\\\\])/', '\\\\$1', $s);
|
|
}
|
|
|
|
function quoted_rsync_include($paths) {
|
|
// note: this function is never called with invalid names because of "if (!$valid_source_path)"
|
|
$result = [];
|
|
foreach ($paths as $path) {
|
|
$valid_path = validname($path);
|
|
$base = rsync_escape_component(basename($valid_path));
|
|
if (is_dir($valid_path) && !is_link($valid_path)) {
|
|
$result[] = "--include=" . escapeshellarg("/{$base}/***");
|
|
} else {
|
|
$result[] = "--include=" . escapeshellarg("/{$base}");
|
|
}
|
|
}
|
|
return implode(' ', $result);
|
|
}
|
|
|
|
// create empty directory for optimized move if not exists
|
|
if (!file_exists($empty_dir)) {
|
|
mkdir($empty_dir);
|
|
}
|
|
|
|
// initialize $move state: null = not a move operation (yet), true = rsync phase, false = cleanup phase
|
|
$move = null;
|
|
|
|
while (true) {
|
|
unset($action, $source, $target, $H, $sparse, $exist, $zfs);
|
|
|
|
// read job parameters from ini file: $action, $title, $source, $target, $H, $sparse, $exist, $zfs (set by emhttp/plugins/dynamix/include/Control.php)
|
|
if (file_exists($active)) extract(parse_ini_file($active));
|
|
|
|
// read PID from file (file_manager may have been restarted)
|
|
if (!$pid && file_exists($pid_file)) {
|
|
$pid = trim(file_get_contents($pid_file));
|
|
}
|
|
|
|
$reply = [];
|
|
if (isset($action)) {
|
|
// check for language changes
|
|
extract(parse_plugin_cfg('dynamix', true));
|
|
if ($display['locale'] != $locale_init) {
|
|
$locale_init = $display['locale'];
|
|
update_translation($locale_init);
|
|
}
|
|
$source = explode("\r", $source);
|
|
switch ($action) {
|
|
case 0: // create folder
|
|
if (!empty($pid)) {
|
|
$reply['status'] = '<i class="fa fa-circle-o-notch fa-spin dfm"></i>'._('Creating').'...';
|
|
} else {
|
|
$dir = $source[0].'/'.$target;
|
|
exec("mkdir -pm0777 ".quoted($dir)." 1>$null 2>$error & echo $!", $pid);
|
|
exec("chown -Rfv nobody:users ".quoted($dir));
|
|
}
|
|
break;
|
|
case 1: // delete folder
|
|
case 6: // delete file
|
|
if (!empty($pid)) {
|
|
$reply['status'] = '<i class="fa fa-circle-o-notch fa-spin dfm"></i>'._('Removing').'... '.exec("tail -1 $status");
|
|
} else {
|
|
exec("find ".quoted($source)." -name \"*\" -print -delete 1>$status 2>$null & echo \$!", $pid);
|
|
}
|
|
break;
|
|
case 2: // rename folder
|
|
case 7: // rename file
|
|
if (!empty($pid)) {
|
|
$reply['status'] = '<i class="fa fa-circle-o-notch fa-spin dfm"></i>'._('Renaming').'...';
|
|
} else {
|
|
$path = dirname($source[0]);
|
|
exec("mv -f ".quoted($source)." ".quoted("$path/$target")." 1>$null 2>$error & echo \$!", $pid);
|
|
}
|
|
break;
|
|
case 3: // copy folder
|
|
case 8: // copy file
|
|
if (!empty($pid)) {
|
|
$reply['status'] = '<i class="fa fa-circle-o-notch fa-spin dfm"></i>'._('Copying').'... '.shell_exec("tail -2 $status | awk -F\"\\r\" '{gsub(/^ +/,\"\",\$NF);print \$NF}'");
|
|
} else {
|
|
$target = validname($target, false);
|
|
if ($target) {
|
|
$mkpath = isdir($target) ? '--mkpath' : '';
|
|
exec("rsync -ahPIX$H $sparse $exist $mkpath --out-format=%f --info=flist0,misc0,stats0,name1,progress2 ".quoted($source)." ".escapeshellarg($target)." 1>$status 2>$error & echo \$!", $pid);
|
|
} else {
|
|
$reply['error'] = 'Invalid target name';
|
|
}
|
|
}
|
|
break;
|
|
case 4: // move folder (rsync)
|
|
case 5: // move folder (mv) - this case needs to be removed from Browse.page
|
|
case 9: // move file (rsync)
|
|
case 10: // move file (mv) - this case needs to be removed from Browse.page
|
|
if (!empty($pid)) {
|
|
// Set move state for resume: true=rsync phase, false=cleanup phase
|
|
if ($move === null) $move = true;
|
|
$reply['status'] = '<i class="fa fa-circle-o-notch fa-spin dfm"></i>'._('Moving').'... '.($move===false ? exec("tail -1 $status") : shell_exec("tail -2 $status | awk -F\"\\r\" '{gsub(/^ +/,\"\",\$NF);print \$NF}'"));
|
|
} else {
|
|
$target = validname($target, false);
|
|
if ($target) {
|
|
$move = true;
|
|
$mkpath = isdir($target) ? '--mkpath' : '';
|
|
|
|
// determine if we can use rsync with rename(2)
|
|
// requirements:
|
|
// 1. all sources and target must be on the same filesystem (device)
|
|
// 2. all sources must have the same parent directory
|
|
// 3. either no --ignore-existing flag OR no conflicts with existing files on target
|
|
$use_rsync_rename = false;
|
|
$last_dirname = '';
|
|
$target_device_id = [];
|
|
|
|
// for filesystem check, find first existing parent directory
|
|
$target_for_stat = $target;
|
|
while (!file_exists($target_for_stat) && $target_for_stat != '/') {
|
|
$target_for_stat = dirname($target_for_stat);
|
|
}
|
|
exec("stat -c %d -- ".escapeshellarg($target_for_stat)." 2>/dev/null", $target_device_id);
|
|
|
|
// check all source paths
|
|
if (!empty($target_device_id)) {
|
|
$use_rsync_rename = true; // assume we can use it, then check for disqualifying conditions
|
|
|
|
foreach ($source as $source_path) {
|
|
|
|
// source path must be valid
|
|
$valid_source_path = validname($source_path);
|
|
if (!$valid_source_path) {
|
|
$use_rsync_rename = false;
|
|
break;
|
|
}
|
|
|
|
// filesystem (device) of source and target must be equal
|
|
$source_device_id = [];
|
|
exec("stat -c %d -- ".escapeshellarg($valid_source_path)." 2>/dev/null", $source_device_id);
|
|
if (empty($source_device_id) || $source_device_id[0] != $target_device_id[0]) {
|
|
$use_rsync_rename = false;
|
|
break;
|
|
}
|
|
|
|
// parent directory of all source paths must be equal (not sure if this is really required, but keeping for now)
|
|
if (!empty($last_dirname) && $last_dirname != dirname($valid_source_path) ) {
|
|
$use_rsync_rename = false;
|
|
break;
|
|
}
|
|
$last_dirname = dirname($valid_source_path);
|
|
|
|
// target must be a directory
|
|
if (!is_dir(rtrim($target,'/'))) {
|
|
$use_rsync_rename = false;
|
|
break;
|
|
}
|
|
|
|
// selected source files and directories must not exist on target when "Overwrite existing files" is not set
|
|
if (!empty($exist)) { // would add "--ignore-existing" to rsync
|
|
$target_item = rtrim($target, '/') . '/' . basename($valid_source_path);
|
|
if (file_exists($target_item)) {
|
|
$use_rsync_rename = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// use rsync rename
|
|
// let rsync act like "mv" by syncing an empty directory against the source location, which moves the files to --backup-dir
|
|
// notes:
|
|
// - existing files are overwritten in --backup-dir (like not using --ignore-existing)
|
|
// - missing directories are created in --backup-dir (like using --mkpath)
|
|
if ($use_rsync_rename) {
|
|
$parent_dir = dirname(validname($source[0]));
|
|
$cmd = "rsync -r --out-format=%f --info=flist0,misc0,stats0,name1,progress2 --delete --backup --backup-dir=".escapeshellarg($target)." ".quoted_rsync_include($source)." --exclude='*' ".escapeshellarg($empty_dir)." ".escapeshellarg($parent_dir)." 1>$status 2>$error & echo \$!";
|
|
exec($cmd, $pid);
|
|
|
|
// use rsync copy-delete
|
|
} else {
|
|
$cmd = "rsync -ahPIX$H $sparse $exist $mkpath --out-format=%f --info=flist0,misc0,stats0,name1,progress2 --remove-source-files ".quoted($source)." ".escapeshellarg($target)." 1>$status 2>$error & echo \$!";
|
|
exec($cmd, $pid);
|
|
}
|
|
|
|
} else {
|
|
$reply['error'] = 'Invalid target name';
|
|
}
|
|
}
|
|
break;
|
|
case 11: // change owner
|
|
if (!empty($pid)) {
|
|
$reply['status'] = '<i class="fa fa-circle-o-notch fa-spin dfm"></i>'._('Updating').'... '.exec("tail -2 $status | grep -Pom1 \"^.+ of '\\K[^']+\"");
|
|
} else {
|
|
exec("chown -Rfv $target ".quoted($source)." 1>$status 2>$error & echo \$!", $pid);
|
|
}
|
|
break;
|
|
case 12: // change permission
|
|
if (!empty($pid)) {
|
|
$reply['status'] = '<i class="fa fa-circle-o-notch fa-spin dfm"></i>'._('Updating').'... '.exec("tail -2 $status | grep -Pom1 \"^.+ of '\\K[^']+\"");
|
|
} else {
|
|
exec("chmod -Rfv $target ".quoted($source)." 1>$status 2>$error & echo \$!", $pid);
|
|
}
|
|
break;
|
|
case 15: // search
|
|
if (!empty($pid)) {
|
|
$reply['status'] = '<i class="fa fa-circle-o-notch fa-spin dfm"></i>'._('Searching').'... '.exec("wc -l $status | grep -Pom1 '^[0-9]+'");
|
|
} else {
|
|
exec("find ".source($source)." -iname ".escapeshellarg($target)." 1>$status 2>$null & echo \$!", $pid);
|
|
}
|
|
break;
|
|
case 99: // kill running background process
|
|
if (!empty($pid)) exec("kill $pid");
|
|
delete_file($active, $pid_file, $status, $error);
|
|
unset($pid, $move);
|
|
break;
|
|
default:
|
|
continue 2;
|
|
}
|
|
$pid = pgrep($pid??0);
|
|
|
|
// Store PID to survive file_manager restarts
|
|
if ($pid !== false) {
|
|
file_put_contents($pid_file, $pid);
|
|
}
|
|
|
|
if ($pid === false) {
|
|
if (!empty($move)) {
|
|
exec("find ".quoted($source)." -type d -empty -print -delete 1>$status 2>$null & echo \$!", $pid);
|
|
$move = false;
|
|
$pid = pgrep($pid);
|
|
} else {
|
|
if ($action != 15) {
|
|
$reply['status'] = _('Done');
|
|
$reply['done'] = 1;
|
|
} else {
|
|
$reply['status'] = cat($status);
|
|
$reply['done'] = 2;
|
|
}
|
|
if ($zfs) {
|
|
$pools = explode("\r",$zfs);
|
|
foreach ($pools as $pool) {
|
|
unset($datasets);
|
|
exec("zfs list -Ho name|grep '^$pool'",$datasets);
|
|
foreach ($datasets as $dataset) if (exec("ls --indicator-style=none /mnt/$dataset|wc -l")==0) exec("zfs destroy $dataset 2>/dev/null");
|
|
}
|
|
}
|
|
if (file_exists($error)) $reply['error'] = str_replace("\n","<br>", trim(file_get_contents($error)));
|
|
delete_file($active, $pid_file, $status, $error);
|
|
unset($pid, $move);
|
|
}
|
|
}
|
|
}
|
|
if (time() - $timer) {
|
|
// update every second
|
|
publish('filemonitor', file_exists($active) ? 1 : 0);
|
|
$timer = time();
|
|
}
|
|
publish('filemanager', json_encode($reply));
|
|
usleep(250000);
|
|
}
|
|
?>
|