From 802950d480285e3ec9f79a88ea8d73f13aa7595b Mon Sep 17 00:00:00 2001 From: zhaixiaojian Date: Tue, 12 Nov 2024 12:08:35 +0800 Subject: [PATCH] * [story#67779,done,1h,0h] Add automatic backup of applications. --- module/instance/control.php | 151 ++++++++++++++++ module/instance/js/autobackup.ui.js | 16 ++ module/instance/model.php | 239 +++++++++++++++++++++++++ module/instance/ui/autobackup.html.php | 56 ++++++ 4 files changed, 462 insertions(+) create mode 100644 module/instance/js/autobackup.ui.js create mode 100644 module/instance/ui/autobackup.html.php diff --git a/module/instance/control.php b/module/instance/control.php index 27dae0624d..786b2434ad 100644 --- a/module/instance/control.php +++ b/module/instance/control.php @@ -532,4 +532,155 @@ class instance extends control $this->loadModel('instance')->syncGitFoxData(); return true; } + + /** + * Instance Backup List. + * 应用备份列表。 + * @param int $id + * @param int $recPerPage + * @param int $pageID + * @return void + */ + public function backupList(int $id, int $recPerPage = 5, int $pageID = 1) + { + /* Initialize Backup List. */ + $instance = $this->instance->getByID($id); + $backupList = $this->instance->backupList($instance); + + /* Pager. */ + $this->app->loadClass('pager', true); + $recTotal = count($backupList); + $pager = new pager($recTotal, $recPerPage, $pageID); + $backupList = array_chunk($backupList, $pager->recPerPage); + $backupList = empty($backupList) ? array() : $backupList[$pageID - 1]; + + $this->view->instance = $instance; + $this->view->pager = $pager; + $this->view->backupList = $backupList; + $this->display(); + } + + /** + * Restore instance by ajax. + * 还原应用。 + * @access public + * @return void + */ + public function ajaxRestore(int $instanceID, string $backupName) + { + if(empty($instanceID) || empty($backupName)) return $this->send(array('result' => 'fail', 'message' => $this->lang->instance->errors->wrongRequestData)); + + $instance = $this->instance->getByID($instanceID); + if(empty($instance)) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->instance->instanceNotExists))); + + $success = $this->instance->restore($instance, $this->app->user, $backupName); + if(!$success) + { + $this->action->create('instance', $instance->id, 'restore', '', json_encode(array('result' => array('result' => 'fail')))); + return $this->send(array('result' => 'fail', 'message' => zget($this->lang->instance->notices, 'restoreFail') )); + } + $this->action->create('instance', $instance->id, 'restore', '', json_encode(array('result' => array('result' => 'success')))); + return $this->send(array('result' => 'success', 'message' => zget($this->lang->instance->notices, 'restoreSuccess'), 'locate' => $this->createLink('instance', 'view', 'id=' . $instanceID))); + } + + /** + * 手动备份。 + * Manual backup. + * + * @param int $id + * @param string $component 0|mysql + * @access public + * @return void + */ + public function manualBackup(int $id) + { + $instance = $this->instance->getByID($id); + $success = $this->instance->backup($instance, $this->app->user); + if(!$success) + { + $this->action->create('instance', $instance->id, 'manualbackup', '', json_encode(array('result' => 'fail'))); + return $this->send(array('result' => 'fail', 'message' => zget($this->lang->instance->notices, 'backupFail'))); + } + $this->action->create('instance', $instance->id, 'manualbackup', '', json_encode(array('result' => 'success'))); + return $this->send(array('result' => 'success', 'message' => zget($this->lang->instance->notices, 'backupSuccess'))); + } + + /** + * 备份设置。 + * backup settings. + * + * @param int $id + * @param string $component 0|mysql + * @access public + * @return void + */ + public function backupSettings(int $instanceID) + { + $instance = $this->instance->getByID($instanceID); + if($_POST) + { + $this->instance->saveBackupSettings($instance); + if(dao::isError()) return $this->send(array('result' => 'fail', 'load' => array('alert' => dao::getError()))); + + return $this->send(array('result' => 'success', 'load' => true, 'closeModal' => true)); + } + $this->view->instance = $instance; + $this->view->backupSettings = $this->instance->getAutoBackupSettings($instanceID); + $this->display(); + } + + /** + * Display or save auto backup. + * 自动备份设置。 + * + * @param int $id + * @access public + * @return void + */ + public function autoBackup(int $instanceID) + { + $instance = $this->instance->getByID($instanceID); + if($_POST) + { + $this->instance->saveAutoBackupSettings($instance); + if(dao::isError()) return $this->send(array('result' => 'fail', 'load' => array('alert' => dao::getError()))); + + $backupSettings = $this->instance->getAutoBackupSettings($instanceID); + $locate = $this->createLink('instance', 'view', 'id=' . $instanceID); + $startTime = strtotime($backupSettings->backupTime); + if($startTime < time()) $startTime = strtotime("+1 day $backupSettings->backupTime"); + if($backupSettings->autoBackup) + { + $startRestoreMessage = sprintf($this->lang->instance->restore->firstStartTime, $instance->name, date('Y-m-d H:i:s', $startTime)); + return $this->send(array('result' => 'success', 'load' => array('alert' => $startRestoreMessage, 'locate' => $locate))); + } + return $this->send(array('result' => 'success', 'load' => array('alert' => $this->lang->instance->backup->disableAutoBackup, 'locate' => $locate))); + } + $this->view->instance = $instance; + $this->view->backupSettings = $this->instance->getAutoBackupSettings($instanceID); + $this->display(); + } + + + /** + * Cron task of auto backup. + * 定时备份。 + * + * @param $instanceID + * @return int|null + */ + public function cronBackup(string $instanceID) + { + $instance = $this->instance->getByID((int)$instanceID); + if(empty($instance)) $this->send(array('result' => 'success', 'message' => $this->lang->instance->instanceNotExists)); + + $sysUser = new stdclass; + $sysUser->account = 'system'; + $success = $this->instance->autoBackup($instance, $sysUser); + if(!$success) + { + return $this->send(array('result' => 'fail', 'message' => zget($this->lang->instance->notices, 'backupFail'))); + } + return $this->send(array('result' => 'success', 'message' => zget($this->lang->instance->notices, 'backupSuccess'))); + } } diff --git a/module/instance/js/autobackup.ui.js b/module/instance/js/autobackup.ui.js new file mode 100644 index 0000000000..e0abb87fda --- /dev/null +++ b/module/instance/js/autobackup.ui.js @@ -0,0 +1,16 @@ +$(function() +{ + $("input[type=checkbox][name='autoBackup']").on('change', function(event) + { + var enabled = $("input[type=checkbox][name='autoBackup']:checked").length > 0; + if(enabled) + { + $('.backup-settings').show(); + } + else + { + $('.backup-settings').hide(); + } + }); + $("input[type=checkbox][name='autoBackup']").trigger("change"); +}); \ No newline at end of file diff --git a/module/instance/model.php b/module/instance/model.php index f0370c715c..669d05626c 100644 --- a/module/instance/model.php +++ b/module/instance/model.php @@ -1151,4 +1151,243 @@ class instanceModel extends model if(empty($this->loadModel('pipeline')->getByNameAndType($name . '-' . $times, $name))) return $name . '-' . $times; } } + + /** + * Backup instance. + * 备份应用实例。 + * @param object $instance + * @param object $user + * @access public + * @return bool + */ + public function backup($instance, $user) + { + $result = $this->cne->backup($instance, $user->account); + if($result->code != 200) return false; + + return true; + } + + /** + * Get backup list of instance. + * 获取应用备份列表。 + * + * @param object $instance + * @access public + * @return array + */ + public function backupList($instance) + { + $result = $this->cne->getBackupList($instance); + if(empty($result) || $result->code != 200 || empty($result->data)) return array(); + + $backupList = $result->data; + usort($backupList, function($backup1, $backup2){ + if($backup1->create_time < $backup2->create_time) return 1; + elseif ($backup1->create_time > $backup2->create_time) return -1; + else return 0; + }); + + $accounts = array_column($backupList, 'creator'); + foreach($backupList as $backup) $accounts = array_merge($accounts, array_column($backup->restores, 'creator')); + + $accounts = array_unique($accounts); + + $users = $this->dao->select('account,realname')->from(TABLE_USER)->where('account')->in($accounts)->fetchPairs('account', 'realname'); + + foreach($backupList as &$backup) + { + $backup->latest_restore_time = 0; + $backup->latest_restore_status = ''; + $backup->username = zget($users, $backup->creator); + /* Mount backup operator info and latest restore info. */ + foreach($backup->restores as &$restore) + { + $restore->username = zget($users, $restore->creator); + + if($restore->create_time > $backup->latest_restore_time) + { + $backup->latest_restore_time = $restore->create_time; + $backup->latest_restore_status = $restore->status; + } + } + + /* Calculate backup size. */ + $backupSize = isset($backup->backup_details->db[0]) ? intval(zget($backup->backup_details->db[0], 'size', 0)) : 0; + $backupSize += isset($backup->backup_details->volume[0]) ? intval(zget($backup->backup_details->volume[0], 'doneBytes', 0)) : 0; + $backup->fmtBackupSize = helper::formatKB($backupSize / 1024); + } + return $backupList; + } + + /** + * Save auto backup settings. + * 保存自动备份配置。 + * @param object $instance + * @access public + * @return bool + */ + public function saveAutoBackupSettings($instance) + { + /* Init Default backup properties. */ + $settings = fixer::input('post') + ->setDefault('autoBackup', '') + ->setDefault('backupTime', '1:00') + ->setDefault('backupCycle', '1') + ->get(); + if(!preg_match("/^([0-2][0-9]):([0-5][0-9])$/", $settings->backupTime, $parts)) + { + dao::$errors[] = $this->lang->instance->backup->invalidTime; + return false; + } + $autoBackup = $settings->autoBackup; + $command = 'moduleName=instance&methodName=cronBackup&instanceID=' . $instance->id; + + /* Disable backup operation. */ + if(!$autoBackup) + { + $this->dao->delete()->from(TABLE_CRON)->where('command')->eq($command)->exec(); + $this->dao->update(TABLE_INSTANCE)->set('autoBackup')->eq($autoBackup)->where('id')->eq($instance->id)->exec(); + if($this->dao->isError()) return false; + + $this->action->create('instance', $instance->id, 'closeautobackup', '', json_encode(array('result' => 'success', 'data' => $settings))); + return true; + } + + /* Enable backup operation. */ + $cron = $this->dao->select('*')->from(TABLE_CRON)->where('command')->eq($command)->fetch(); + + list($hour, $minute) = explode(':', $settings->backupTime); + $cronData = new stdclass; + $cronData->m = intval($minute); + $cronData->h = intval($hour); + $cronData->dom = '*'; + $cronData->mon = '*'; + $cronData->dow = '*'; + $cronData->type = 'zentao'; + $cronData->status = 'normal'; + $cronData->command = $command; + $cronData->remark = $this->lang->instance->backup->cronRemark; + + /* Save cron task. */ + if($cron) $this->dao->update(TABLE_CRON)->autoCheck()->data($cronData)->where('id')->eq($cron->id)->exec(); + else $this->dao->insert(TABLE_CRON)->data($cronData)->exec(); + if($this->dao->isError()) return false; + + /* Save instance backup settings. */ + $this->dao->update(TABLE_INSTANCE)->set('autoBackup')->eq($settings->autoBackup)->where('id')->eq($instance->id)->exec(); + if($this->dao->isError()) return false; + + $this->action->create('instance', $instance->id, 'openautobackup', '', json_encode(array('result' => 'success', 'data' => $settings))); + return true; + } + + /** + * Save backup settings. + * 保存备份设置。 + * + * @param $instance + * @return void + */ + public function saveBackupSettings($instance) + { + $settings = fixer::input('post')->trim('backupKeepDays')->get(); + if($instance->backupKeepDays == $settings->backupKeepDays) return true; + + /* Save instance backup settings. */ + $this->dao->update(TABLE_INSTANCE)->set('backupKeepDays')->eq($settings->backupKeepDays)->where('id')->eq($instance->id)->exec(); + if($this->dao->isError()) return false; + + $this->action->create('instance', $instance->id, 'savebackupsettings', '', json_encode(array('result' => 'success', 'data' => $settings))); + return true; + } + + /** + * Get auto backup settings. + * 获取自动备份设置。 + * + * @param int $instnaceID + * @access public + * @return object + */ + public function getAutoBackupSettings($instanceID) + { + $instance = $this->getByID($instanceID); + $cron = $this->dao->select('*')->from(TABLE_CRON)->where('command')->eq('moduleName=instance&methodName=cronBackup&instanceID=' . $instance->id)->limit(1)->fetch(); + if(!$cron) $cron = new stdclass; + + $hour = substr('0' . zget($cron, 'h', '1'), -2, 2); + $minute = substr('0' . zget($cron, 'm', '00'), -2, 2); + + $settings = new stdclass; + $settings->backupTime = "{$hour}:{$minute}"; + $settings->autoBackup = boolval(zget($instance, 'autoBackup', false)); + $settings->cycleDays = 1; // Cycle days is always is 1 at present. + return $settings; + } + + /** + * Restore instance. + * 还原应用实例。 + * @param object $instance + * @param object $user + * @param string $backupName + * @access public + * @return bool + */ + public function restore($instance, $user, $backupName) + { + $result = $this->cne->restore($instance, $backupName, $user->account); + if ($result->code != 200) return false; + return true; + } + + /** + * auto backup. + * 自动备份。 + * + * @param $instance + * @param $user + * @return void + */ + public function autoBackup($instance, $user) + { + $cron = $this->dao->select('*')->from(TABLE_CRON)->where('command')->eq('moduleName=instance&methodName=cronBackup&instanceID=' . $instance->id)->limit(1)->fetch(); + if(!$cron) + { + $this->action->create('instance', $instance->id, 'autobackup', '', json_encode(array('result' => 'fail', 'data'=> $cron))); + return false; + } + + /* 1. Call automatic backup*/ + $result = $this->cne->backup($instance, $user->account); + if($result->code != 200) + { + $this->action->create('instance', $instance->id, 'autobackup', '', json_encode(array('result' => 'fail', 'data' => $result))); + return false; + } + $this->action->create('instance', $instance->id, 'autobackup', '', json_encode(array('result' => 'success', 'data' => $result))); + + /* 2. Pick latest successful backup recorder. */ + $latestBackup = null; + $backupList = $this->backupList($instance); + foreach($backupList as $backup) + { + if(empty($latestBackup) or $backup->status == 'completed' && $backup->create_time > $latestBackup->create_time) $latestBackup = $backup; + } + + /* 3. delete expired backup. Get backup list of instance, then check every backup is expired or not.*/ + foreach($backupList as $backup) + { + if($latestBackup && $latestBackup->name == $backup->name) continue; // Keep latest successful backup. + + $deadline = intval($backup->create_time) + $instance->backupKeepDays * 24 * 3600; + if($deadline < time()) + { + $result = $this->cne->deleteBackup($instance, $backup->name); + $this->action->create('instance', $instance->id, 'deleteexpiredbackup', '', json_encode(array('result' => 'success', 'data' => array('backupName' => $backup->name, 'backupCreateTime' => $backup->create_time)))); + } + } + return true; + } } diff --git a/module/instance/ui/autobackup.html.php b/module/instance/ui/autobackup.html.php new file mode 100644 index 0000000000..2a5ddfee1e --- /dev/null +++ b/module/instance/ui/autobackup.html.php @@ -0,0 +1,56 @@ + + * @package instance + * @link https://www.zentao.net + */ + +namespace zin; + +formPanel +( + set::id('automaticBackupForm'), + set::title($lang->instance->backup->operators['auto']), + set::submitBtnText($lang->save), + set::actions(array('submit')), + formRow + ( + formGroup + ( + set::name('autoBackup'), + set::width('500px'), + set::control(array('control' => 'checkbox', 'value' => '1', 'text' => $lang->instance->restore->enableAutoRestore, 'checked' => $backupSettings->autoBackup ? 'true' : '')), + ) + ), + formRow + ( + formGroup + ( + set::name('backupTime'), + set::width('20px'), + set::required(true), + set::class('backup-settings'), + set::label($lang->instance->backup->backupTime), + set::control(array('control' => 'time')), + set::value($backupSettings->backupTime) + ) + ), + formRow + ( + formGroup + ( + set::name('backupCycle'), + set::width('20px'), + set::required(true), + set::control('picker'), + set::class('backup-settings'), + set::label($lang->instance->backup->cycleDays), + set::items($lang->instance->backup->cycleList), + set::value($backupSettings->cycleDays) + ) + ) +);