backup = $this->wire()->database->backups(); include(__DIR__ . '/ProcessDatabaseBackups.info.php'); /** @var array $info */ $this->labels = array( "module-title" => $info['title'], "info" => $this->_('Info'), "download" => $this->_('SQL file'), "downloadZIP" => $this->_('ZIP file'), "backup" => $this->_('Backup'), "delete" => $this->_('Delete'), "restore" => $this->_('Restore'), "cancel" => $this->_('Cancel'), "upload" => $this->_('Upload'), "description" => $this->_('Description'), "valid" => $this->_('Valid?'), "time" => $this->_('Date/Time'), "user" => $this->_('Exported by'), "size" => $this->_('File size'), "pathname" => $this->_('Filename'), "dbName" => $this->_('Database name'), "tables" => $this->_('Which tables?'), "numTables" => $this->_('Num tables exported'), "numCreateTables" => $this->_('Num tables created'), "numInserts" => $this->_('Num rows'), "numSeconds" => $this->_('Export time (seconds)'), ); } /** * Get the path where backup files are stored * * @param bool $short Specify true if you only want path relative to site root (for display purposes) * @return string * */ protected function backupPath($short = false) { $path = $this->backup->getPath(); if($short) $path = str_replace($this->wire('config')->paths->root, '/', $path); return $path; } /** * Get array of all backup files * * @param string $onlyID Specify ID of file if you only want to get a specific backup file * @return array Array of file info arrays indexed by file 'id' * */ protected function getBackupFiles($onlyID = '') { $files = array(); $n = 0; foreach($this->backup->getFiles() as $file) { $id = "$n:$file"; if($onlyID && $id != $onlyID) continue; $info = $this->backup->getFileInfo($file); $info['id'] = $id; $files[$id] = $info; $n++; } return $files; } /** * Get actions for given $file array * * @param array $file File info array * @return array * */ protected function getFileActions(array $file) { $url = $this->wire('page')->url(); $actions = array( 'info' => array( 'href' => $url . "info/?id=$file[id]", 'label' => $this->labels['info'], 'icon' => 'info-circle', 'secondary' => false, 'head' => false, ), 'download' => array( 'href' => $url . "download/?id=$file[id]", 'label' => $this->labels['download'], 'icon' => 'cloud-download', 'secondary' => false, 'head' => true, ), 'downloadZIP' => array( 'href' => $url . "download/?id=$file[id]&zip=1", 'label' => $this->labels['downloadZIP'], 'icon' => 'download', 'secondary' => false, 'head' => true, ), 'restore ' => array( 'href' => $url . "restore/?id=$file[id]", 'label' => $this->labels['restore'], 'icon' => 'life-ring', 'secondary' => true, 'head' => false, ), 'delete' => array( 'href' => $url . "delete/?id=$file[id]", 'label' => $this->labels['delete'], 'icon' => 'trash', 'secondary' => true, 'head' => false, ), ); if(!class_exists("\\ZipArchive")) unset($actions['downloadZIP']); return $actions; } /** * Get information about file requested in URL via $_GET['id'] or given file array * * @param array $file Omit to get file specified in GET[id] * @return array * @throws WireException * */ protected function getFile(array $file = null) { if($file === null) { $id = $this->input->get('id'); if(is_null($id)) throw new WireException("No file specified"); $files = $this->getBackupFiles(); if(!isset($files[$id])) throw new WireException("Unrecognized file"); $file = $files[$id]; } if(empty($file['pathname'])) { throw new WireException('Backup file missing pathname index'); } if(empty($file['zip'])) { $basename = basename($file['pathname'], '.sql'); $file['zip'] = dirname($file['pathname']) . '/' . $basename . '.zip'; } return $file; } /** * This function is executed when a page with your Process assigned is accessed. * * This can be seen as your main or index function. You'll probably want to replace * everything in this function. * */ public function ___execute() { $sanitizer = $this->wire()->sanitizer; $modules = $this->wire()->modules; $input = $this->wire()->input; $numFiles = 0; $backupFiles = $this->getBackupFiles(); $this->headline($this->labels['module-title']); /** @var InputfieldForm $form */ $form = $modules->get('InputfieldForm'); /** @var InputfieldCheckbox $checkbox */ $checkbox = $modules->get('InputfieldCheckbox'); $checkbox->attr('name', 'deletes[]'); $checkbox->addClass('delete-checkbox'); $checkbox->checkboxOnly = true; $checkbox->label = ' '; /** @var MarkupAdminDataTable $table */ $table = $modules->get('MarkupAdminDataTable'); $table->setEncodeEntities(false); $table->headerRow(array( $this->_x('file', 'th'), $this->_x('date', 'th'), $this->_x('tables', 'th'), $this->_x('rows', 'th'), $this->_x('size', 'th'), $this->_x('actions', 'th'), wireIconMarkup('trash', 'lg') )); foreach($backupFiles as $id => $file) { $numFiles++; $numTables = $file['numTables']; if($numTables && !count($file['tables'])) $numTables .= " " . $this->_('(all)'); $basename = $file['basename']; $time = $file['time'] ? $file['time'] : $file['mtime']; if($file['description']) $basename .= '*'; $actions = array(); foreach($this->getFileActions($file) as $action) { $actions[] = $this->aTooltip($action['href'], wireIconMarkup($action['icon'], 'fw'), $action['label']); } $checkbox->attr('id', "delete_" . $sanitizer->fieldName($id)); $checkbox->attr('value', $id); $table->row(array( $this->nowrap($basename) => "./info/?id=$id", $this->tdSort(strtotime($time), wireRelativeTimeStr($time, true)), $this->nowrap($numTables), $this->tdSort($file['numInserts'], number_format($file['numInserts'])), $this->tdSort($file['size'], wireBytesStr($file['size'])), $this->nowrap(implode(' ', $actions)), $checkbox->render(), )); } if(!$numFiles) $form->description = $this->_('No database backup files yet.'); $form->value = $table->render(); /** @var InputfieldButton $f */ $f = $modules->get('InputfieldButton'); $f->value = $this->labels['backup']; $f->icon = 'database'; $f->href = "./backup/"; $f->addClass('btn-backup'); $f->showInHeader(true); $form->add($f); /** @var InputfieldButton $f */ $f = $modules->get('InputfieldButton'); $f->value = $this->labels['upload']; $f->href = "./upload/"; $f->icon = 'cloud-upload'; $f->addClass('btn-upload'); $f->setSecondary(); $f->showInHeader(true); $form->add($f); /** @var InputfieldButton $f */ $f = $modules->get('InputfieldSubmit'); $f->attr('name', 'submit_delete_checked'); $f->value = $this->_('Delete checked'); $f->addClass('btn-delete-checked'); $f->icon = 'trash'; $f->showInHeader(true); $form->add($f); if($input->post('submit_delete_checked') && is_array($input->post('deletes'))) { // process deleted backups $form->processInput($input->post); // for CSRF only foreach($input->post('deletes') as $id) { if(!isset($backupFiles[$id])) continue; /** @var array $backupFile */ $backupFile = $backupFiles[$id]; $this->unlinkBackup($backupFile); } $this->wire()->session->redirect('./'); } return $form->render(); } /** * Execute upload * * @return string * */ public function ___executeUpload() { $modules = $this->wire()->modules; $input = $this->wire()->input; /** @var InputfieldForm $form */ $form = $modules->get('InputfieldForm'); $form->attr('id', 'upload_form'); $form->description = $this->_('Add new SQL database dump file'); /** @var InputfieldFile $f */ $f = $modules->get("InputfieldFile"); $f->name = 'upload_file'; $f->label = $this->_('Upload File'); $f->extensions = 'sql'; $f->maxFiles = 0; $f->unzip = 1; $f->overwrite = false; $f->destinationPath = $this->backupPath(); if(method_exists($f, 'setMaxFilesize')) $f->setMaxFilesize('100g'); $form->add($f); /** @var InputfieldSubmit $b */ $b = $modules->get('InputfieldSubmit'); $b->attr('name', 'submit_upload_file'); $b->attr('value', $this->labels['upload']); $form->add($b); if($input->post('submit_upload_file')) { $form->processInput($input->post); foreach($f->value as $pagefile) { $this->message(sprintf($this->_('Added file: %s'), $pagefile->basename)); } $this->session->redirect($this->wire()->page->url); } return $form->render(); } /** * Execute backup info * * @return string * */ public function ___executeInfo() { $modules = $this->wire()->modules; $config = $this->wire()->config; $file = $this->getFile(); $this->headline($file['basename']); $info = $this->backup->getFileInfo($file['pathname']); $info = array_merge($file, $info); if($info['valid']) { $info['valid'] = $this->_('Yes! Confirmed valid begin and end of file.'); if(!count($info['tables'])) $info['tables'] = array($this->_('All Tables')); } else { $info['valid'] = $this->_('Unable to confirm if valid file (likely not created by this tool)'); } unset($info['basename'], $info['id'], $info['zip']); $info['pathname'] = str_replace($config->paths->root, '/', $info['pathname']); if(empty($info['time'])) { $info['mtime'] = date('Y-m-d H:i:s') . " (" . wireRelativeTimeStr($info['mtime']) . ")"; } else { unset($info['mtime']); $time = strtotime($info['time']); $info['time'] = "$info[time] (" . wireRelativeTimeStr($time) . ")"; } $bytes = $info['size']; $info['size'] = number_format($bytes) . " " . $this->_x('bytes', 'file-details'); if(function_exists("ProcessWire\\wireBytesStr")) { $info['size'] .= ' (' . wireBytesStr($bytes) . ')'; } /** @var MarkupAdminDataTable $table */ $table = $modules->get('MarkupAdminDataTable'); foreach($info as $key => $value) { if(is_array($value)) $value = implode(', ', $value); if(!strlen($value)) continue; $label = isset($this->labels[$key]) ? $this->labels[$key] : $key; $table->row(array($label, $value)); } /** @var InputfieldForm $form */ $form = $modules->get('InputfieldForm'); $form->value = $table->render(); $n = 0; foreach($this->getFileActions($file) as $name => $action) { if($name === 'info') continue; /** @var InputfieldButton $f */ $f = $modules->get('InputfieldButton'); $f->href = $action['href']; $f->value = $action['label']; $f->icon = $action['icon']; if($action['secondary']) $f->setSecondary(); if($action['head']) $f->showInHeader(true); $form->add($f); $n++; } return $form->render(); } /** * Execute backup * * @return string * */ public function ___executeBackup() { $input = $this->wire()->input; $session = $this->wire()->session; $modules = $this->wire()->modules; $allTables = $this->backup->getAllTables(); $this->headline($this->labels['backup']); if($input->post('submit_backup') && ($input->post('backup_all') || count($input->post('tables')))) { $this->processBackup(); $session->redirect('../'); } /** @var InputfieldForm $form */ $form = $modules->get('InputfieldForm'); /** @var InputfieldName $f */ $f = $modules->get('InputfieldName'); $f->attr('name', 'backup_name'); $f->label = $this->_('Backup name'); $f->description = $this->_('This will be used for the backup filename. The extension .sql will be added to it automatically.'); $f->notes = $this->_('If omitted, a unique filename will be automatically generated.'); $f->required = false; // $f->attr('value', $this->wire('config')->dbName . '_' . date('Y-m-d')); $form->add($f); /** @var InputfieldText $f */ $f = $modules->get('InputfieldText'); $f->attr('name', 'description'); $f->label = $this->_('Backup description'); $f->collapsed = Inputfield::collapsedBlank; $form->add($f); /** @var InputfieldCheckbox $f */ $f = $modules->get('InputfieldCheckbox'); $f->attr('name', 'backup_all'); $f->label = $this->_('Backup all tables?'); $f->attr('value', 1); $f->attr('checked', 'checked'); $form->add($f); /** @var InputfieldSelectMultiple $f */ $f = $modules->get('InputfieldSelectMultiple'); $f->attr('name', 'tables'); $f->label = $this->_('Tables'); $f->description = $this->_('By default, the export will include all tables. If you only want certain tables to be included, select them below.'); foreach($allTables as $table) $f->addOption($table, $table); $f->attr('value', $allTables); $f->showIf = 'backup_all=0'; $form->add($f); /** @var InputfieldSubmit $f */ $f = $modules->get('InputfieldSubmit'); $f->attr('name', 'submit_backup'); $f->icon = 'database'; $f->showInHeader(true); $form->add($f); $form->appendMarkup = "

" . $this->_('Please be patient after clicking submit. Backups may take some time, depending on how much there is to backup.') . "

"; return $form->render(); } /** * Process submitted backup form, creating a new backup file * */ protected function processBackup() { $input = $this->wire()->input; $config = $this->wire()->config; $sanitizer = $this->wire()->sanitizer; $allTables = $this->backup->getAllTables(); $filename = basename($sanitizer->filename($input->post('backup_name')), '.sql'); if(empty($filename)) $filename = $config->dbName; $_filename = $filename; $filename .= '.sql'; if(preg_match('/^(.+)-(\d+)$/', $_filename, $matches)) { $_filename = $matches[1]; $n = $matches[2]; } else { $n = 0; } while(file_exists($this->backupPath() . $filename)) { $filename = $_filename . "-" . (++$n) . ".sql"; } $options = array( 'filename' => $filename, 'description' => $sanitizer->text($input->post('description')), ); if(!$input->post('backup_all')) { // selective tables $options['tables'] = array(); foreach($input->post('tables') as $table) { if(!isset($allTables[$table])) continue; $options['tables'][] = $allTables[$table]; } } $file = $this->backup->backup($options); if($file) $this->message($this->_('Saved new backup:') . " $file"); } /** * Execute download * */ public function ___executeDownload() { $input = $this->wire()->input; $session = $this->wire()->session; $file = $this->getFile(); $getZIP = $input->get('zip') && !empty($file['zip']); if($getZIP && !is_file($file['zip'])) { $zipInfo = wireZipFile($file['zip'], array($file['pathname'])); if(!empty($zipInfo['errors']) || !is_file($file['zip'])) { foreach($zipInfo['errors'] as $error) $this->error($error); $this->error(sprintf($this->_('Failed to create ZIP file: %s'), $file['zip'])); if(is_file($file['zip'])) $this->unlink($file['zip']); $session->redirect($this->wire()->page->url); return; } } $filename = $getZIP ? $file['zip'] : $file['pathname']; wireSendFile($filename, array( 'forceDownload' => true, 'exit' => false )); if($getZIP && is_file($file['zip'])) { $this->unlink($file['zip']); } exit; } /** * Execute delete * * @return string * */ public function ___executeDelete() { $input = $this->wire()->input; $modules = $this->wire()->modules; $session = $this->wire()->session; $page = $this->wire()->page; $this->headline($this->_('Delete Backup')); $file = $this->getFile(); $submitDelete = $input->post('submit_delete'); if($submitDelete && $file && $input->post('delete_confirm')) { // confirmed delete $this->unlinkBackup($file); $session->redirect($page->url); } else if($submitDelete) { // not confirmed $session->redirect($page->url); } else { // render confirmation form /** @var InputfieldForm $form */ $form = $modules->get('InputfieldForm'); $form->action = "./?id=$file[id]"; $form->description = sprintf($this->_('Delete %s?'), $file['basename']); /** @var InputfieldCheckbox $f */ $f = $modules->get('InputfieldCheckbox'); $f->attr('name', 'delete_confirm'); $f->label = $this->_('Check the box to confirm'); $form->add($f); /** @var InputfieldSubmit $f */ $f = $modules->get('InputfieldSubmit'); $f->attr('name', 'submit_delete'); $form->add($f); return $form->render(); } return ''; } /** * Execute restore * * @return string * */ public function ___executeRestore() { $input = $this->wire()->input; $session = $this->wire()->session; $modules = $this->wire()->modules; $page = $this->wire()->page; $file = $this->getFile(); $this->headline($this->_('Restore Backup')); if($input->post('submit_restore')) { if($input->post('restore_confirm') && file_exists($file['pathname'])) { $options = array( 'allowDrop' => true, 'maxSeconds' => 3600 ); if($input->post('restore_drop')) $options['dropAll'] = true; $success = $this->backup->restore($file['basename'], $options); if($success) { $this->message(sprintf($this->_('Restored: %s'), "$file[basename]")); $session->redirect($page->url); } else { $this->error(sprintf($this->_('Error restoring: %s'), "$file[pathname]")); } } } else { /** @var InputfieldForm $form */ $form = $modules->get('InputfieldForm'); $form->action = "./?id=$file[id]"; $form->description = $this->_('Warning: the current database will be destroyed and replaced (this has potential to break your site!)'); /** @var InputfieldCheckbox $f */ $f = $modules->get('InputfieldCheckbox'); $f->attr('name', 'restore_confirm'); $f->label = sprintf($this->_('Restore %s?'), $file['basename']); $form->add($f); if(method_exists($this->backup, 'dropAllTables')) { /** @var InputfieldCheckbox $f */ $f = $modules->get('InputfieldCheckbox'); $f->attr('name', 'restore_drop'); $f->label = $this->_('Drop all tables from current database before restore?'); $f->showIf = 'restore_confirm=1'; $form->add($f); } /** @var InputfieldSubmit $f */ $f = $modules->get('InputfieldSubmit'); $f->attr('name', 'submit_restore'); $form->add($f); /** @var InputfieldButton $f */ $f = $modules->get('InputfieldButton'); $f->attr('value', $this->labels['cancel']); $f->href = $page->url; $f->setSecondary(true); $form->add($f); return $form->render(); } return ''; } /** * Render a sortable column for a list table * * @param string|int $unformatted Unformatted sortable value * @param string $formatted Formatted value * @return string * */ protected function tdSort($unformatted, $formatted) { return "$unformatted" . $this->nowrap($formatted); } /** * Rener a nowrap span with given markup * * @param string $markup * @return string * */ protected function nowrap($markup) { return "$markup"; } /** * Render an link with tooltip * * @param string $href Link URL * @param string $label Link text * @param string $description Tooltip text * @return string * */ protected function aTooltip($href, $label, $description) { return "$label"; } /** * Delete/unlink DB file from file array * * @param array $file * @throws WireException * */ protected function unlinkBackup(array $file) { if(empty($file['zip'])) $file = $this->getFile($file); $pathnames = array($file['pathname'], $file['zip']); foreach($pathnames as $pathname) { if(empty($pathname) || !is_file($pathname)) continue; if($this->unlink($pathname)) { $this->message(sprintf($this->_('Deleted: %s'), basename($pathname))); } } } /** * Unlink filename * * @param string $pathname * @return bool * @throws WireException * */ protected function unlink($pathname) { if(!is_string($pathname)) throw new WireException('pathname must be a string'); $fileTools = $this->wire()->files; if(method_exists($fileTools, 'unlink')) return $fileTools->unlink($pathname); return unlink($pathname); } /** * Install * * @throws WireException * */ public function ___install() { // check that they have the required PW version if(version_compare($this->wire()->config->version, self::minVersion, '<')) { throw new WireException("This module requires ProcessWire " . self::minVersion . " or newer."); } parent::___install(); } /** * Uninstall * */ public function ___uninstall() { $path = $this->backupPath(true); $this->warning("Please note that the backup files in $path remain. If you don’t want them there, please remove them manually."); parent::___uninstall(); } }