3 * Bacula(R) - The Network Backup Solution
4 * Baculum - Bacula web interface
6 * Copyright (C) 2013-2017 Kern Sibbald
8 * The main author of Baculum is Marcin Haba.
9 * The original author of Bacula is Kern Sibbald, with contributions
10 * from many others, a complete list can be found in the file AUTHORS.
12 * You may use this file and others of this release according to the
13 * license defined in the LICENSE file, which includes the Affero General
14 * Public License, v3.0 ("AGPLv3") and some additional permissions and
15 * terms pursuant to its AGPLv3 Section 7.
17 * This notice must be preserved when any source code is
18 * conveyed and/or propagated.
20 * Bacula(R) is a registered trademark of Kern Sibbald.
23 Prado::using('Application.Web.Class.BaculumWebPage');
24 Prado::using('System.Exceptions.TException');
25 Prado::using('System.Web.UI.WebControls.TWizard');
26 Prado::using('System.Web.UI.WebControls.TDataGrid');
27 Prado::using('System.Web.UI.ActiveControls.TActiveLinkButton');
28 Prado::using('System.Web.UI.ActiveControls.TActiveImageButton');
29 Prado::using('System.Web.UI.ActiveControls.TDropContainer');
30 Prado::using('System.Web.UI.ActiveControls.TDraggable');
31 Prado::using('System.Web.UI.ActiveControls.TActiveDataGrid');
32 Prado::using('System.Web.UI.ActiveControls.TCallback');
33 Prado::using('System.Web.UI.ActiveControls.TActiveTextBox');
35 class RestoreWizard extends BaculumWebPage
38 * Job levels allowed to restore.
41 private $joblevel = array('F', 'I', 'D');
43 * Job statuses allowed to restore.
45 private $jobstatus = array('T', 'W', 'A', 'E', 'e', 'f');
48 * File browser special directories.
50 private $browser_root_dir = array('name' => '.', 'type' => 'dir', 'fileid' => null);
51 private $browser_up_dir = array('name' => '..', 'type' => 'dir');
54 * Used to provide in template selected by user single jobid to restore.
56 public $restore_single_jobid;
59 * Stores file relocation option. Used in template.
61 public $file_relocation_opt;
64 * FIle browser elements for which 'Add' button is unavailable.
66 public $excluded_elements_from_add = array('.', '..');
69 * Prefix for Bvfs path.
71 const BVFS_PATH_PREFIX = 'b2';
73 public function onInit($param) {
74 parent::onInit($param);
75 if(!$this->IsPostBack && !$this->IsCallBack) {
77 $this->loadBackupClients();
82 * Wizard next button callback actions.
84 * @param TWizard $sender sender object
85 * @param TWizardNavigationEventParameter $param sender parameters
88 public function wizardNext($sender, $param) {
89 if ($param->CurrentStepIndex === 0) {
90 $this->loadBackupsForClient();
91 $this->loadGroupBackupToRestore();
92 $this->loadGroupFileSetToRestore();
93 $this->loadRestoreClients();
94 if (isset($_SESSION['restore_single_jobid'])) {
95 $this->restore_single_jobid = $_SESSION['restore_single_jobid'];
97 } elseif ($param->CurrentStepIndex === 1) {
98 if ($this->Request->contains('backup_to_restore')) {
99 $_SESSION['restore_single_jobid'] = $this->Request['backup_to_restore'];
101 $this->setFileVersions();
102 $this->loadSelectedFiles();
103 $this->loadFileVersions();
105 } elseif ($param->CurrentStepIndex === 2) {
106 $this->loadRequiredVolumes();
107 } elseif ($param->CurrentStepIndex === 3) {
108 if (isset($_SESSION['file_relocation'])) {
109 $this->file_relocation_opt = $_SESSION['file_relocation'];
111 } elseif ($param->CurrentStepIndex === 4) {
112 if ($this->Request->contains('file_relocation')) {
113 $_SESSION['file_relocation'] = $this->Request['file_relocation'];
115 $this->file_relocation_opt = $_SESSION['file_relocation'];
120 * Wizard prev button callback actions.
122 * @param TWizard $sender sender object
123 * @param TWizardNavigationEventParameter $param sender parameters
126 public function wizardPrev($sender, $param) {
127 if ($param->CurrentStepIndex === 2) {
128 $this->restore_single_jobid = $_SESSION['restore_single_jobid'];
129 $this->loadBackupsForClient();
130 } elseif ($param->CurrentStepIndex === 3) {
131 $this->setFileVersions();
132 $this->loadSelectedFiles();
133 $this->loadFileVersions();
135 } elseif ($param->CurrentStepIndex === 5) {
136 $this->file_relocation_opt = $_SESSION['file_relocation'];
145 public function wizardStop($sender, $param) {
146 $this->resetWizard();
147 $this->goToDefaultPage();
151 * Load backup clients list (step 1).
153 * @param TActiveDropDownList $sender sender object
154 * @param TCommandParameter $param parameters object
157 public function loadBackupClients() {
158 $client_list = array();
159 $clients = $this->getModule('api')->get(array('clients'))->output;
160 for ($i = 0; $i < count($clients); $i++) {
161 $client_list[$clients[$i]->name] = $clients[$i]->name;
164 $this->BackupClientName->dataSource = $client_list;
165 $this->BackupClientName->dataBind();
169 * Load restore client list.
173 public function loadRestoreClients() {
174 $client_list = array();
175 $clients = $this->getModule('api')->get(array('clients'))->output;
176 for ($i = 0; $i < count($clients); $i++) {
177 $client_list[$clients[$i]->name] = $clients[$i]->name;
179 $this->RestoreClient->SelectedValue = $this->BackupClientName->SelectedValue;
180 $this->RestoreClient->dataSource = $client_list;
181 $this->RestoreClient->dataBind();
185 * Load backups for selected client (Step 2).
189 public function loadBackupsForClient() {
190 $clientid = $this->getBackupClientId();
191 $jobs_for_client = $this->getModule('api')->get(array('clients', 'jobs', $clientid))->output;
192 $jobs = $this->getModule('misc')->objectToArray($jobs_for_client);
193 $this->BackupsToRestore->DataSource = array_filter($jobs, array($this, 'isBackupJobToRestore'));
194 $this->BackupsToRestore->dataBind();
198 * Check if job can be used in restore.
200 * @param array $job job properties
201 * @return true if job should be listed to restore, otherwise false
203 private function isBackupJobToRestore($job) {
204 return ($job['type'] === 'B' && in_array($job['level'], $this->joblevel) && in_array($job['jobstatus'], $this->jobstatus));
207 public function loadBackupSelection($sender, $param) {
208 $this->GroupBackupToRestoreField->Display = ($sender->ID == $this->GroupBackupSelection->ID) ? 'Dynamic' : 'None';
209 $this->BackupToRestoreField->Display = ($sender->ID == $this->OnlySelectedBackupSelection->ID) ? 'Dynamic' : 'None';
210 $this->setBrowserFiles();
211 $this->setFileVersions();
212 $this->setFilesToRestore();
213 $this->markFileToRestore(null, null);
214 $_SESSION['restore_path'] = array();
219 * Get selected backup client identifier.
221 * @return mixed client identifier or null if no clientid found
223 public function getBackupClientId() {
225 $clients = $this->getModule('api')->get(array('clients'))->output;
226 for ($i = 0; $i < count($clients); $i++) {
227 if ($clients[$i]->name === $this->BackupClientName->SelectedValue) {
228 $clientid = $clients[$i]->clientid;
236 * Load backup jobs to restore for group most recent backups feature.
240 public function loadGroupBackupToRestore() {
241 $jobs = $this->getModule('api')->get(array('jobs'))->output;
242 $jobs = $this->getModule('misc')->objectToArray($jobs);
243 $clientid = $this->getBackupClientId();
244 $job_group_list = array();
245 for ($i = 0; $i < count($jobs); $i++) {
246 $job = $this->getModule('misc')->objectToArray($jobs[$i]);
247 if ($this->isBackupJobToRestore($jobs[$i]) && $jobs[$i]['clientid'] === $clientid) {
248 $job_group_list[$jobs[$i]['name']] = $jobs[$i]['name'];
252 $this->GroupBackupToRestore->dataSource = $job_group_list;
253 $this->GroupBackupToRestore->dataBind();
257 * Load filesets to restore for group most recent backups feature.
261 public function loadGroupFileSetToRestore() {
262 $filesets = $this->getModule('api')->get(array('filesets', 'info'))->output;
263 $fileset_list = array();
264 for ($i = 0; $i < count($filesets); $i++) {
265 $fileset_list[$filesets[$i]->filesetid] = $filesets[$i]->fileset . ' (' . $filesets[$i]->createtime . ')';
267 asort($fileset_list);
269 $this->GroupBackupFileSet->dataSource = $fileset_list;
270 $this->GroupBackupFileSet->dataBind();
274 * Prepare left file browser content.
278 private function prepareBrowserContent() {
279 $jobids = $this->getElementaryBackup();
281 if (!empty($jobids)) {
282 // generating Bvfs may take a moment
283 $this->generateBvfsCache($jobids);
285 // get directories list
286 $bvfs_dirs = $this->getModule('api')->set(
287 array('bvfs', 'lsdirs'),
288 array('jobids' => $jobids, 'path' => implode($_SESSION['restore_path']))
290 $dirs = $this->getModule('misc')->parseBvfsList($bvfs_dirs->output);
293 $bvfs_files = $this->getModule('api')->set(
294 array('bvfs', 'lsfiles'),
295 array('jobids' => $jobids, 'path' => implode($_SESSION['restore_path']))
297 $files = $this->getModule('misc')->parseBvfsList($bvfs_files->output);
299 $elements = array_merge($dirs, $files);
300 if(count($_SESSION['restore_path']) > 0) {
301 array_unshift($elements, $this->browser_root_dir);
304 $this->loadBrowserFiles($elements);
308 * Get single elementary backup job identifiers.
310 * @return string comma separated job identifiers
312 private function getElementaryBackup() {
314 if($this->OnlySelectedBackupSelection->Checked && isset($_SESSION['restore_single_jobid'])) {
315 $jobs = $this->getModule('api')->get(
316 array('bvfs', 'getjobids', $_SESSION['restore_single_jobid'])
318 $ids = is_object($jobs) ? $jobs->output : array();
319 foreach ($ids as $jobid) {
320 if(preg_match('/^([\d\,]+)$/', $jobid, $match) == 1) {
329 $this->GroupBackupToRestore->SelectedValue,
331 $this->BackupClientName->SelectedValue,
333 $this->GroupBackupFileSet->SelectedValue
335 $jobs_recent = $this->getModule('api')->get($params);
336 if (count($jobs_recent->output) > 0) {
337 $ids = $jobs_recent->output;
338 $jobids = implode(',', $ids);
345 * Load path callback method.
346 * Used for manually typed paths in path field.
348 * @param TActiveLinkButton $sender sender object
349 * @param TEventParameter $param events parameter
352 public function loadPath($sender, $param) {
353 $path = explode('/', $this->PathField->Text);
354 $path_len = count($path);
355 for ($i = 0; $i < count($path); $i++) {
356 if ($i == ($path_len - 1) && empty($path[$i])) {
357 // last path dir is slash so not add slash to last element
362 $this->goToPath($path, true);
366 * Go to specific path in the file browser.
367 * There is possible to pass both single directory 'somedir'
368 * or whole path '/etc/somedir'.
370 * @param string $path path to go
371 * @param bool $full_path determines if $path param is full path or relative path (singel directory)
374 private function goToPath($path = '', $full_path = false) {
375 if(!empty($path) && !$full_path) {
376 if($path == $this->browser_up_dir['name']) {
377 array_pop($_SESSION['restore_path']);
378 } elseif($path == $this->browser_root_dir['name']) {
379 $_SESSION['restore_path'] = array();
381 array_push($_SESSION['restore_path'], $path);
384 if ($full_path && is_array($path)) {
385 $_SESSION['restore_path'] = $path;
387 $this->setBrowserPath();
388 $this->prepareBrowserContent();
392 * Add/mark file to restore.
393 * Used as callback to drag&drop browser elements.
395 * @param object $sender sender object
396 * @param object $param param object
399 public function addFileToRestore($sender, $param) {
401 $source_element_id = null;
402 $file_prop = array();
403 if (isset($param->CallbackParameter)) {
404 $id_parts = explode('_', $sender->ClientID, 6);
405 $source_element_id = $id_parts[3];
406 $fileid = $param->CallbackParameter;
408 $control = $param->getDroppedControl();
409 $item = $control->getNamingContainer();
410 $id_parts = explode('_', $param->getDragElementID(), 6);
411 $source_element_id = $id_parts[3];
413 if($source_element_id == $this->VersionsDataGrid->ID) {
414 if (is_null($fileid)) {
415 $fileid = $this->VersionsDataGrid->getDataKeys()->itemAt($item->getItemIndex());
417 $file_prop = $this->getFileVersions($fileid);
419 if (is_null($fileid)) {
420 $fileid = $this->DataGridFiles->getDataKeys()->itemAt($item->getItemIndex());
422 $file_prop = $this->getBrowserFile($fileid);
424 if($file_prop['name'] != $this->browser_root_dir['name'] && $file_prop['name'] != $this->browser_up_dir['name']) {
425 $this->markFileToRestore($fileid, $file_prop);
426 $this->loadSelectedFiles();
431 * Remove file from files marked to restre.
433 * @param TActiveImageButton $sender remove button object
434 * @param TEventParameter $param param object
437 public function removeSelectedFile($sender, $param) {
438 $fileid = $param->CallbackParameter;
439 $this->unmarkFileToRestore($fileid);
440 $this->loadSelectedFiles();
444 * Get file backed up versions.
445 * Called as callback on file element click.
447 * @param TCallback $sender sender object
448 * @param object $param param object
451 public function getVersions($sender, $param) {
452 list($filename, $pathid, $filenameid, $jobid) = explode('|', $param->CallbackParameter, 4);
453 if($filenameid == 0) {
454 $this->goToPath($filename);
457 $clientname = $this->BackupClientName->SelectedValue;
458 $versions = $this->getModule('api')->get(array('bvfs', 'versions', $clientname, $jobid, $pathid, $filenameid))->output;
459 $file_versions = $this->getModule('misc')->parseFileVersions($filename, $versions);
460 $this->setFileVersions($file_versions);
461 $this->VersionsDataGrid->dataSource = $file_versions;
462 $this->VersionsDataGrid->dataBind();
463 $this->loadSelectedFiles();
467 * Refresh/re-render selected files list.
469 * @param TDropContainer $sender sender object
470 * @param TEventParameter $param param object
473 public function refreshSelectedFiles($sender, $param) {
474 $this->loadSelectedFiles();
475 $this->SelectedVersionsDropper->render($param->NewWriter);
479 * Load file browser files to list.
481 * @param array $files files to list.
484 private function loadBrowserFiles($files) {
485 $this->setBrowserFiles($files);
486 $this->DataGridFiles->dataSource = $files;
487 $this->DataGridFiles->dataBind();
491 * Load file versions area.
495 private function loadFileVersions() {
496 $this->VersionsDataGrid->dataSource = $_SESSION['files_versions'];
497 $this->VersionsDataGrid->dataBind();
501 * Load selected files in drop area.
505 private function loadSelectedFiles() {
506 $this->SelectedVersionsDataGrid->dataSource = $_SESSION['restore'];
507 $this->SelectedVersionsDataGrid->dataBind();
511 * Set file browser path field.
515 private function setBrowserPath() {
516 $this->PathField->Text = implode($_SESSION['restore_path']);
520 * Generate Bvfs cache by job identifiers.
522 * @param string $jobids comma separated job identifiers
525 private function generateBvfsCache($jobids) {
526 $this->getModule('api')->set(
527 array('bvfs', 'update'),
528 array('jobids' => $jobids)
533 * Set versions for selected file.
535 * @param array $versions file versions data
538 private function setFileVersions($versions = array()) {
539 $_SESSION['files_versions'] = $versions;
543 * Get file versions for specified fileid.
545 * @param integer $fileid file identifier
548 private function getFileVersions($fileid) {
550 foreach($_SESSION['files_versions'] as $file) {
551 if(array_key_exists('fileid', $file) && $file['fileid'] == $fileid) {
562 * @param array $files file list
565 private function setBrowserFiles($files = array()) {
566 $_SESSION['files_browser'] = $files;
570 * Get browser file by fileid.
572 * @param integer $fileid file identifier
575 private function getBrowserFile($fileid) {
577 foreach($_SESSION['files_browser'] as $file) {
578 if(array_key_exists('fileid', $file) && $file['fileid'] == $fileid) {
587 * Mark file to restore.
589 * @param integer $fileid file identifier
590 * @param array $file file properties to mark
593 private function markFileToRestore($fileid, $file) {
594 if($fileid === null) {
595 $_SESSION['restore'] = array();
596 } elseif($file['name'] != $this->browser_root_dir['name'] && $file['name'] != $this->browser_up_dir['name']) {
597 $_SESSION['restore'][$fileid] = $file;
602 * Unmark file to restore.
604 * @param integer $fileid file identifier
607 private function unmarkFileToRestore($fileid) {
608 if(array_key_exists($fileid, $_SESSION['restore'])) {
609 unset($_SESSION['restore'][$fileid]);
614 * Get files to restore.
616 * @return array list with files to restore
618 public function getFilesToRestore() {
619 return $_SESSION['restore'];
623 * Set files to restore
625 * @param array $files files to restore
628 public function setFilesToRestore($files = array()) {
629 $_SESSION['restore'] = $files;
633 * Get all restore elements (fileids and dirids).
635 * @param bool $as_object return result as object
636 * @return array list fileids and dirids
638 public function getRestoreElements($as_object = false) {
642 foreach ($this->getFilesToRestore() as $fileid => $properties) {
643 if ($properties['type'] == 'dir') {
644 $dirids[] = $properties['pathid'];
645 } elseif ($properties['type'] == 'file') {
646 $fileids[] = $fileid;
647 if ($properties['lstat']['linkfi'] !== 0) {
648 $findexes[] = $properties['jobid'] . ',' . $properties['lstat']['linkfi'];
652 $ret = array('fileid' => $fileids, 'dirid' => $dirids, 'findex' => $findexes);
653 if($as_object === true) {
660 * Wizard finish method.
664 public function wizardCompleted() {
665 $jobids = $this->getElementaryBackup();
666 $path = self::BVFS_PATH_PREFIX . getmypid();
667 $restore_elements = $this->getRestoreElements();
668 $cmd_props = array('jobids' => $jobids, 'path' => $path);
670 if(count($restore_elements['fileid']) > 0) {
671 $cmd_props['fileid'] = implode(',', $restore_elements['fileid']);
674 if(count($restore_elements['dirid']) > 0) {
675 $cmd_props['dirid'] = implode(',', $restore_elements['dirid']);
678 if (count($restore_elements['findex']) > 0) {
679 $cmd_props['findex'] = implode(',', $restore_elements['findex']);
685 $this->getModule('api')->create(array('bvfs', 'restore'), $cmd_props);
686 $restore_props = array();
687 $restore_props['rpath'] = $path;
688 $restore_props['client'] = $this->RestoreClient->SelectedValue;
689 $restore_props['priority'] = intval($this->RestoreJobPriority->Text);
690 if ($_SESSION['file_relocation'] == 2) {
691 if (!empty($this->RestoreStripPrefix->Text)) {
692 $restore_props['strip_prefix'] = $this->RestoreStripPrefix->Text;
694 if (!empty($this->RestoreAddPrefix->Text)) {
695 $restore_props['add_prefix'] = $this->RestoreAddPrefix->Text;
697 if (!empty($this->RestoreAddSuffix->Text)) {
698 $restore_props['add_suffix'] = $this->RestoreAddSuffix->Text;
700 } elseif ($_SESSION['file_relocation'] == 3) {
701 if (!empty($this->RestoreRegexWhere->Text)) {
702 $restore_props['regex_where'] = $this->RestoreRegexWhere->Text;
705 if (!array_key_exists('add_prefix', $restore_props)) {
706 $restore_props['where'] = $this->RestorePath->Text;
708 $restore_props['replace'] = $this->ReplaceFiles->SelectedValue;
709 $restore_props['restorejob'] = $this->RestoreJob->SelectedValue;
711 $ret = $this->getModule('api')->create(array('jobs', 'restore'), $restore_props);
712 $jobid = $this->getModule('misc')->findJobIdStartedJob($ret->output);
714 $url_params = array('open' => 'Job');
715 if (is_numeric($jobid)) {
716 $url_params['id'] = $jobid;
718 $this->goToDefaultPage($url_params);
722 * Load restore jobs on the list.
726 private function loadRestoreJobs() {
727 $restore_job_tasks = $this->getModule('api')->get(array('jobs', 'tasks', 'type', 'R'))->output;
729 foreach ($restore_job_tasks as $director => $restore_jobs) {
730 $jobs = array_merge($jobs, $restore_jobs);
732 $this->RestoreJob->DataSource = array_combine($jobs, $jobs);
733 $this->RestoreJob->dataBind();
736 private function loadRequiredVolumes() {
738 foreach ($this->getFilesToRestore() as $fileid => $props) {
739 // it can be expensive for many restore paths
740 $result = $this->getModule('api')->get(array('volumes', 'required', $props['jobid'], $fileid));
741 if ($result->error === 0) {
742 for ($i = 0; $i < count($result->output); $i++) {
743 $volumes[$result->output[$i]->volume] = array(
744 'volume' => $result->output[$i]->volume,
745 'inchanger' => $result->output[$i]->inchanger
750 $this->RestoreVolumes->DataSource = array_values($volumes);
751 $this->RestoreVolumes->dataBind();
756 * All fields are back to initial form.
760 private function resetWizard() {
761 $this->setBrowserFiles();
762 $this->setFileVersions();
763 $this->setFilesToRestore();
764 $this->markFileToRestore(null, null);
765 $this->loadRestoreJobs();
766 $_SESSION['restore_path'] = array();
767 $_SESSION['restore_single_jobid'] = null;
768 unset($_SESSION['file_relocation']);