#!/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 mb_strimhalf($text, $width, $trim_marker = "") {
  if (mb_strlen($text) <= $width) return $text;
  if ($width < 8) return mb_substr($text, 0, $width);
  $marker_len = mb_strlen($trim_marker);
  $available = $width - $marker_len;
  $half = (int)($available / 2);
  return mb_substr($text, 0, $half) . $trim_marker . mb_substr($text, -($available - $half));
}

function time_to_seconds($time_str) {
  if (!preg_match('/^(\d+):(\d+):(\d+)$/', $time_str, $matches)) {
    return null;
  }
  return intval($matches[1]) * 3600 + intval($matches[2]) * 60 + intval($matches[3]);
}

function seconds_to_time($seconds) {
  $hours = intval($seconds / 3600);
  $minutes = intval(($seconds % 3600) / 60);
  $secs = $seconds % 60;
  return sprintf("%d:%02d:%02d", $hours, $minutes, $secs);
}

function calculate_eta($transferred, $percent, $speed, $last_rsync_eta_seconds = null) {
  // Convert transferred size to bytes
  $multipliers = ['K' => 1024, 'M' => 1024*1024, 'G' => 1024*1024*1024, 'T' => 1024*1024*1024*1024];
  $transferred_bytes = floatval($transferred);
  foreach ($multipliers as $unit => $mult) {
    if (stripos($transferred, $unit) !== false) {
      $transferred_bytes *= $mult;
      break;
    }
  }

  // Convert speed to bytes/sec
  $speed_bytes = floatval($speed);
  if (stripos($speed, 'kB/s') !== false) {
    $speed_bytes *= 1024;
  } elseif (stripos($speed, 'MB/s') !== false) {
    $speed_bytes *= 1024 * 1024;
  } elseif (stripos($speed, 'GB/s') !== false) {
    $speed_bytes *= 1024 * 1024 * 1024;
  }

  // Calculate total size from percent
  $percent_val = intval(str_replace('%', '', $percent));
  
  // At 0%, we cannot calculate ETA reliably from transferred/percent
  // but we can use the last known rsync ETA if available
  if ($percent_val == 0) {
    if ($last_rsync_eta_seconds !== null && $last_rsync_eta_seconds > 0) {
      return seconds_to_time($last_rsync_eta_seconds);
    }
    return "N/A";
  }
  
  if ($percent_val > 0 && $percent_val < 100 && $speed_bytes > 1.0) {
    $total_bytes = $transferred_bytes * 100 / $percent_val;
    $remaining_bytes = $total_bytes - $transferred_bytes;
    $calculated_eta_seconds = intval($remaining_bytes / $speed_bytes);

    // Apply hysteresis: blend with last known rsync ETA to smooth out fluctuations
    if ($last_rsync_eta_seconds !== null && $last_rsync_eta_seconds > 0) {
      // Weight: 70% last rsync ETA, 30% calculated ETA
      $eta_seconds = intval($last_rsync_eta_seconds * 0.7 + $calculated_eta_seconds * 0.3);
    } else {
      $eta_seconds = $calculated_eta_seconds;
    }

    return seconds_to_time($eta_seconds);
  }

  return "N/A";
}

/**
 * Parse rsync progress output and track transfer statistics.
 *
 * Uses static variables to maintain state across multiple calls during a single operation.
 * Call with $reset=true before starting a new copy/move operation to clear previous state.
 *
 * @param string $status Raw rsync output line to parse
 * @param string $action_label Label to display for the current action (e.g. "Copying", "Moving")
 * @param bool $reset If true, resets static state variables (call before new operation)
 * @return array Associative array with progress information: 'text', 'percent', 'eta', 'speed', etc.
 */
function parse_rsync_progress($status, $action_label, $reset = false) {
  static $last_rsync_eta_seconds = null;
  static $total_size = null;
  static $total_calculations = [];  // Store multiple calculations for averaging
  static $last_calc_percent = null; // Track last percent we calculated from

  // Reset static variables when starting a new transfer
  if ($reset) {
    $last_rsync_eta_seconds = null;
    $total_size = null;
    $total_calculations = [];
    $last_calc_percent = null;
    return [];
  }

  // initialize text array with action label
  $text[0] = $action_label . "... ";

  // obtain progress line like:
  // currently running (timestamp represents total ETA):
  // "         37.91G   4%   58.59MB/s    3:47:20"
  // transfer of single file finished (note: timestamp now represents total elapsed time):
  // "         37.93G   4%   53.80MB/s    0:11:12 (xfr#32, to-chk=554/596)"
  // note: we do not loop with PHP as there could be a huge amount of progress lines
  // note: leading space in regex ensures we only match progress lines, not filename lines
  // note: tac --before prevents line concatenation when $status lacks trailing \n
  $progress_line = trim(exec("tac --before $status | grep -m1 -E '^ .*[0-9]+%.*[0-9]+:[0-9]+:[0-9]+' | tr -s ' ' || echo ''"));
  if ($progress_line) {
    $parts = explode(' ', $progress_line);
    if (count($parts) >= 4) {
      $transferred = $parts[0];
      $percent = $parts[1];
      $speed = $parts[2];
      $time = $parts[3];

      // Calculate total size by averaging multiple measurements
      // rsync truncates percent (not rounds), so 47.9% shows as 47%
      // This causes ~2% error per percent point. We average 5 measurements at different percents.
      if ($total_size === null || count($total_calculations) < 5) {
        $percent_val = intval(str_replace('%', '', $percent));
        
        // Track if this is a "running transfer" line (no xfr# info)
        $is_running_line = !isset($parts[4]);
        
        // Calculate total when we have at least 3% progress on a running transfer line
        // and the percent changed since last calculation
        if ($is_running_line && $percent_val >= 3 && $last_calc_percent !== $percent_val) {
          // Convert transferred size to bytes
          $multipliers = ['K' => 1024, 'M' => 1024*1024, 'G' => 1024*1024*1024, 'T' => 1024*1024*1024*1024];
          $transferred_bytes = floatval($transferred);
          foreach ($multipliers as $unit => $mult) {
            if (stripos($transferred, $unit) !== false) {
              $transferred_bytes *= $mult;
              break;
            }
          }
          
          // Calculate total from transferred and percent (rsync truncates, so add 0.5% for better accuracy)
          $calculated_total = $transferred_bytes * 100 / ($percent_val + 0.5);
          $total_calculations[] = $calculated_total;
          $last_calc_percent = $percent_val;
          
          // Once we have 5 measurements, average them and lock the value
          if (count($total_calculations) >= 5) {
            $total_size = array_sum($total_calculations) / count($total_calculations);
          }
        }
      }

      // Check if this is an ETA line or elapsed time line
      // ETA lines have only 4 parts, elapsed time lines have additional (xfr#...) info
      if (isset($parts[4])) {
        // Elapsed time line - calculate our own ETA with hysteresis
        $time = calculate_eta($transferred, $percent, $speed, $last_rsync_eta_seconds);
      } else {
        // ETA line from rsync - store it for hysteresis
        $last_rsync_eta_seconds = time_to_seconds($time);
      }

      // Build progress text with total size
      $progress_parts = [];
      $progress_parts[] = _('Completed') . ": " . $percent;
      $progress_parts[] = _('Speed') . ": " . $speed;
      $progress_parts[] = _('ETA') . ": " . $time;
      
      // Always show Total (either calculated or N/A)
      if ($total_size !== null) {
        // Format total size for display
        $total_display = $total_size;
        $unit_display = 'B';
        $units = ['KB' => 1024, 'MB' => 1024*1024, 'GB' => 1024*1024*1024, 'TB' => 1024*1024*1024*1024];
        foreach (array_reverse($units, true) as $unit => $divisor) {
          if ($total_size >= $divisor) {
            $total_display = $total_size / $divisor;
            $unit_display = $unit;
            break;
          }
        }
        $progress_parts[] = _('Total') . ": ~" . number_format($total_display, 2) . $unit_display;
      } else {
        $progress_parts[] = _('Total') . ": N/A";
      }
      
      $text[1] = implode(",  ", $progress_parts);
    }
  }

  // obtain filename line like:
  // "/mnt/disk6/images/image.jpg"
  // note: -v is used to obtain the opposite of the progress line regex and to filter out empty lines
  $file_line = exec("tac $status | grep -m1 -v -E '(^\$|^ .*[0-9]+%.*[0-9]+:[0-9]+:[0-9]+)' | tr -s ' ' || echo ''");
  if ($file_line) {
    $text[0] .= htmlspecialchars(mb_strimhalf($file_line, 70, '...'), ENT_QUOTES, 'UTF-8');
  }

  return $text;

}

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);}

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 "rsync rename" if not exists
// note: as only file_manager creates and uses this directory no "is empty" check and rebuild mechanism is required
if (!file_exists($empty_dir)) {
  mkdir($empty_dir);
}

// initialize $delete_empty_dirs state: null = not a move operation (yet), true = rsync copy-delete phase, false = cleanup phase (done)
$delete_empty_dirs = null;

// infinite loop to monitor and execute file operations
// Note: exec() uses /bin/sh which is symlinked to bash in unraid and a requirement for process substitution syntax >(...)
while (true) {
  unset($action, $source, $target, $H, $sparse, $exist, $zfs);

  // read job parameters from JSON file: $action, $title, $source, $target, $H, $sparse, $exist, $zfs (set by emhttp/plugins/dynamix/include/Control.php)
  if (file_exists($active)) {
    $json = file_get_contents($active);
    $data = json_decode($json, true);
    if (is_array($data)) {
      extract($data);
    } elseif ($json !== false && trim($json) !== '') {
      // Log JSON parse failure for debugging (non-empty file that failed to parse)
      exec('logger -t file_manager "Warning: Failed to parse active job JSON: ' . escapeshellarg(substr($json, 0, 100)) . '"');
    }
  }

  // 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

      // return status of running action
      if (!empty($pid)) {
        $reply['status'] = json_encode([
          'action' => $action,
          'text' => [htmlspecialchars(mb_strimhalf(exec("tail -1 $status"), 70, '...'), ENT_QUOTES, 'UTF-8')]
        ]);

      // start action
      } 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

      // return status of running action
      if (!empty($pid)) {
        $reply['status'] = json_encode([
          'action' => $action,
          'text' => parse_rsync_progress($status, _('Copying'))
        ]);

      // start action
      } else {
        parse_rsync_progress(null, null, true); // Reset static variables
        $target = validname($target, false);
        if ($target) {
          $mkpath = isdir($target) ? '--mkpath' : '';
          $cmd = "rsync -ahPIX$H $sparse $exist $mkpath --no-inc-recursive --out-format=%f --info=flist0,misc0,stats0,name1,progress2 ".quoted($source)." ".escapeshellarg($target)." > >(stdbuf -o0 tr '\\r' '\\n' >$status) 2>$error & echo \$!";
          // note: rsync defaults to "inc_recurse mode" which causes creating the whole directory structure at target before transfering files.
          //       This is a problem because those target directories get temporary permissions of "root:root" and "chmod 0700", which
          //       breaks access for all unraid users through /mnt/user/sharename. We avoid this behavior by using "--no-inc-recursive".
          exec($cmd, $pid);
        } else {
          $reply['error'] = _('Invalid target name');
        }
      }
      break;
    case 4: // move folder
    case 9: // move file

      // return status of running action
      if (!empty($pid)) {

        // set delete_empty_dirs state for resume after file_manager restart (only relevant for rsync copy-delete))
        // note: would be theoretically enabled for "rsync rename", too, so find would even then try to delete 
        //       source empty directories which do not exist, but as "rsync-rename" is very fast and the file_manager
        //       runs for several seconds in the background even if the user directly closes the WebGUI it should be
        //       nearly impossible that the user is able to see any errors
        if ($delete_empty_dirs === null) $delete_empty_dirs = true;

        // cleanup empty directories: simple status
        if ($delete_empty_dirs === false) {
          $reply['status'] = json_encode([
            'action' => $action,
            'text' => [htmlspecialchars(mb_strimhalf(exec("tail -1 $status"), 70, '...'), ENT_QUOTES, 'UTF-8')]
          ]);

        // moving: progress
        } else {
          $reply['status'] = json_encode([
            'action' => $action,
            'text' => parse_rsync_progress($status, _('Moving'))
          ]);
        }

      // start action
      } else {
        parse_rsync_progress(null, null, true); // Reset static variables
        $target = validname($target, false);
        if ($target) {
          $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) { // this can be a file or directory

              // 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;
                }
              }

              // target must not be a subdirectory of any source (backup-dir should be outside source tree)
              $source_dirname = is_dir($valid_source_path) ? $valid_source_path : dirname($valid_source_path);
              if (strpos(rtrim($target,'/') . '/', rtrim($source_dirname,'/') . '/') === 0) {
                $reply['error'] = _('Cannot move directory into its own subdirectory');
                $use_rsync_rename = false;
                break 2; // break out of both: foreach and case
              }

            }
          }

          // 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)
          // - rsync prefixes the moved files with "deleting " in the output, which we strip with sed, to not confuse the user
          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)." > >(stdbuf -o0 tr '\\r' '\\n' | sed 's/^deleting //' >$status) 2>$error & echo \$!";
            exec($cmd, $pid);

          // use rsync copy-delete
          } else {
            $delete_empty_dirs = true; // cleanup needed: rsync --remove-source-files leaves empty directories
            $cmd = "rsync -ahPIX$H $sparse $exist $mkpath --no-inc-recursive --out-format=%f --info=flist0,misc0,stats0,name1,progress2 --remove-source-files ".quoted($source)." ".escapeshellarg($target)." > >(stdbuf -o0 tr '\\r' '\\n' >$status) 2>$error & echo \$!";
            // note: rsync defaults to "inc_recurse mode" which causes creating the whole directory structure at target before transfering files.
            //       This is a problem because those target directories get temporary permissions of "root:root" and "chmod 0700", which
            //       breaks access for all unraid users through /mnt/user/sharename. We avoid this behavior by using "--no-inc-recursive".
            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);
      $delete_empty_dirs = null;
      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($delete_empty_dirs)) {
        exec("find ".quoted($source)." -type d -empty -print -delete 1>$status 2>$null & echo \$!", $pid);
        $delete_empty_dirs = 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);
        $delete_empty_dirs = null;
      }
    }
  }
  if (time() - $timer) {
    // update every second
    publish('filemonitor', file_exists($active) ? 1 : 0);
    $timer = time();
  }
  publish('filemanager', json_encode($reply));
  usleep(250000);
}
?>
