* @Copyright (c) 2020~2023 https://www.aieok.com All rights reserved. * 升级包规定的目录结构 * xxx_版本号.zip(如:xxx_1.0.0.zip) * | * |————runtime * | | * | |___update.sql(更新脚本) //create table test(id init(11)); create table test2(id init(11)); * | |___rockback.sql(回滚脚本) //drop table test; //drop table test2; * | |___remove.txt // clear清除目录和文件 * | * |____php * */ namespace app\admin\controller\system; use app\common\controller\AdminController; use think\facade\View; use think\facade\Request; use think\facade\Db; use taoler\com\Api; use taoler\com\Str; use taoler\com\Files; use think\facade\Config; use think\facade\Log; use app\common\lib\SqlFile; use app\common\lib\Zip; use taoser\SetArr; class Upgrade extends AdminController { protected $root_dir = "../"; //站点代码的根目录 protected $backup_dir = "../runtime/update/backup_dir/"; //备份目录 protected $upload_dir = "../runtime/update/upload_dir/"; //升级包目录 protected $sys_version_num; //当前系统的版本 protected $sys_version = ''; protected $pn = ''; protected $sys = ''; public function __construct() { parent::initialize(); $this->sys_version = Config::get('taoler.version'); $this->pn = Config::get('taoler.appname'); $this->sys = $this->getSystem(); } /** 升级界面 */ public function index() { //字符隐藏 $key = Str::func_substr_replace($this->sys['key']); $sys_base = [ 'key' => $key, 'upcheck_url' => $this->sys['upcheck_url'], 'upgrade_url' => $this->sys['upgrade_url'], ]; View::assign('ver_num',$sys_base); return View::fetch(); } //设置key public function key() { $data = Request::only(['key']); if(empty($data['key'])){ return json(['code'=>0,'msg'=>'请填写正确的key']); } $res = Db::name('system')->cache('system')->update(['key'=>$data['key'],'id'=>1]); if($res){ $res = ['code'=>0,'msg'=>'保存成功']; } else { $res = ['code'=>-1,'msg'=>'保存失败']; } return json($res); } //修改key public function keyedit() { $key = Db::name('system')->field('key,upcheck_url,upgrade_url')->find(1); if(Request::isAjax()){ $data = Request::only(['key','upcheck_url','upgrade_url']); if(empty($data['key'])){ return json(['code'=>-1,'msg'=>'请正确填写申请到的key']); } $res = Db::name('system')->cache('system')->update(['key'=>$data['key'],'upcheck_url'=>$data['upcheck_url'],'upgrade_url'=>$data['upgrade_url'],'id'=>1]); if($res){ $res = ['code'=>0,'msg'=>'修改成功']; } else { $res = ['code'=>-1,'msg'=>'修改失败']; } return json($res); } View::assign('key',$key); return View::fetch(); } //升级前的版本检测 public function check() { $cy = Api::urlPost($this->sys['base_url'],['u'=>$this->sys['domain']]); if($cy->code == 0 && $cy->level !== $this->sys['clevel']){ Db::name('system')->cache('system')->update(['clevel'=>$cy->level,'id'=>1]); } $versions = Api::urlPost($this->sys['upcheck_url'],['pn'=>$this->pn,'ver'=>$this->sys_version]); // 判断服务器状态 $version_code = $versions->code; if($version_code == -1){ $res = json(['code'=>$version_code,'msg'=>$versions->msg]); } if($version_code == 1){ $res = json(['code'=>$version_code,'msg'=>$versions->msg,'data'=>['version'=>$versions->version,'upnum'=>$versions->up_num,'info'=>$versions->info]]); } if($version_code == 0){ $res = json(['code'=>$version_code,'msg'=>$versions->msg]); } return $res; } /** * 在线更新 * 1.提交接口2.下载数据包3.备份4升级 */ public function upload() { $versions = Api::urlPost($this->sys['upgrade_url'],['url'=>$this->sys['domain'],'key'=>$this->sys['key'],'pn'=>$this->pn,'ver'=>$this->sys_version]); Log::channel('update')->info('update:{type} {progress} {msg}',['type'=>'check','progress'=>'0%','msg'=>'===>升级检测开始===>']); //判断服务器状态 $version_code = $versions->code; if($version_code == -1){ Log::channel('update')->info('update:{type} {progress} {msg}',['type'=>'check eroor','progress'=>'5%','msg'=>'===>服务器链接失败===>']); return json(['code'=>$version_code,'msg'=>$versions->msg]); } $version_num = $versions->version; $file_url = $versions->src; //判断远程文件是否可用存在 $header = get_headers($file_url, true); if(!isset($header[0]) && (strpos($header[0], '200') || strpos($header[0], '304'))){ return json(['code'=>-1,'msg'=>'获取远程文件失败']); } $upload_dir = Files::getDirPath($this->upload_dir); //拼接升级文件暂存路径 Files::mkdirs($upload_dir); $package_file = $upload_dir.'taoler_'.$version_num.'.zip'; //本地zip文件 $cpfile = copy($file_url,$package_file); //把远程文件放入本地 if(!$cpfile) { return json(['code'=>-1,'msg'=>'下载升级文件失败']); } //记录下日志 Log::channel('update')->info('update:{type} {progress} {msg}',['type'=>'success','progress'=>'20%','msg'=>'上传升级包'.$version_num.'成功!']); //升级前备份代码 $ex = array('.git','.idea','runtime','data','addons','config','extend','mysql','public','vendor','view'); // 排除备份目录 $this->backFile($this->root_dir,$this->backup_dir,$ex); //执行升级 $updateRes = $this->execute_update($package_file); $upDate = $updateRes->getData(); if($upDate['code'] == -1){ return json(['code'=>-1,'msg'=>$upDate['msg']]); } //清除 Files::delDirAndFile($this->upload_dir); Files::delDirAndFile($this->backup_dir); //清除废弃目录和文件 $delFiles = '../runtime/remove.txt'; if(file_exists($delFiles)){ $str = file_get_contents($delFiles); //将整个文件内容读入到一个字符串中 $str = str_replace("\r\n",",",$str); $delArr = explode(',',$str); foreach($delArr as $v){ if(is_dir($v)){ //删除文件夹 Files::delDirAndFile($v.'/',true); } else { //删除文件 if(file_exists($v)){ unlink($v); } } } unlink($delFiles); } //更新系统的版本号了 //更新php的版本号了(应该跟svn/git的版本号一致) //更新数据库的版本号了(应该跟svn/git的版本号一致) $value = [ 'version' => $version_num ]; $res = SetArr::name('taoler')->edit($value); if(!$res){ return json(['code'=>-1,'msg'=>'代码更新成功,但版本写入失败']); } Log::channel('update')->info('update:{type} {progress} {msg}',['type'=>'success','progress'=>'100%','msg'=>'升级成功!']); //清理缓存 $this->clearSysCache(); return json(['code'=>0,'msg'=>'升级成功']); } /** * 升级执行 * @param string $package_file * @return \think\response\Json * @throws \Exception */ private function execute_update(string $package_file) { // 1.解压 zip文件有密码的话需要解密 $zip = new Zip; $zipDir = strstr($package_file, '.zip',true); //返回文件名后缀前的字符串 $zipPath = Files::getDirPath($zipDir); //转换为带/的路径 压缩文件解压到的路径 $unzip_res = $zip->unzip($package_file,$zipPath); if(!$unzip_res) { return json(['code'=>-1,'msg'=>'解压失败']); } //解压成功,得到文件夹 //$package_name = str_replace('.zip','',$package_file); Log::channel('update')->info('update:{type} {progress} {msg}',['type'=>'success','progress'=>'50%','msg'=>'升级文件解压成功!']); // 2.升级sql操作 $upSql = $zipPath.'runtime/update.sql'; if(file_exists($upSql)) { try{ SqlFile::dbExecute($upSql); } catch (\Exception $e){ return json(['code'=>-1,'msg'=>$e->getMessage()]); } //删除sql语句 unlink($upSql); } // 3.升级PHP if(is_dir($zipPath)) { //升级前的写入文件权限检查 $allUpdateFiles = Files::getAllFile($zipPath); if (empty($allUpdateFiles)) return json(['code' => -1, 'msg' => '无可更新文件。']); $checkString = ''; foreach ($allUpdateFiles as $updateFile) { $coverFile = ltrim(str_replace($zipPath, '', $updateFile), DIRECTORY_SEPARATOR); $dirPath = dirname('../'.$coverFile); if (file_exists('../'.$coverFile)) { if (!is_writable('../'.$coverFile)) $checkString .= $coverFile . ' [' . '无写入权限' . ']
'; } else { if (!is_dir($dirPath)) @mkdir($dirPath, 0777, true); if (!is_writable($dirPath)) $checkString .= $dirPath . ' [' . '无写入权限' . ']
'; } } if (!empty($checkString)) return json(['code' => -1, 'msg' => $checkString]); $cpRes = Files::copyDirs($zipPath,$this->root_dir); $cpData = $cpRes->getData(); //更新失败 if($cpData['code'] == -1) { //数据库回滚 /* if(file_exists($this->upload_dir.'/'.$package_file.'/mysql/mysql_rockback.sql')) { $this->database_operation($this->upload_dir.'/'.$package_file.'/mysql/mysql_rockback.sql'); } */ //php代码回滚 升级前备份的代码 Files::copydirs($this->backup_dir, $this->root_dir); return json(['code'=>-1,'msg'=>$cpData['msg']]); } Log::channel('update')->info('update:{type} {progress} {msg}',['type'=>'success','progress'=>'70%','msg'=>'升级文件执行成功!']); } return json(['code'=>0,'msg'=>'升级文件执行成功']); } /** * 手动处理升级包上传 */ public function uploadZip() { $files = request()->file('file'); $mime = $files->getMime(); if($mime !== 'application/zip'){ return json(['code'=>-1,'msg'=>'文件类型不对']); } $name = $files->getOriginalName(); //校验后缀 $ext = pathinfo($name,PATHINFO_EXTENSION); //文件后缀 if($ext != 'zip') { return json(['code'=>-1,'msg'=>'上传文件格式不对']); } //对比版本号 $fname = pathinfo($name,PATHINFO_FILENAME); //无后缀文件名 $version_num = array_pop(explode('_',$fname)); $verRes = version_compare($version_num,$this->sys_version,'>'); if(!$verRes){ return json(['code'=>-1,'msg'=>'不能降级,请选择正确版本']); } $upDir = $this->upload_dir.$fname; //$mv = $files->move('/../tmp/web/upload_dir',$version_num); $mfile = move_uploaded_file($files,$upDir); if(!$mfile) { return json(['code'=>0,'msg'=>'上传文件失败']); } //升级前备份代码 $ex = array('.git','.idea','runtime','data','addons','config','extend'); // 排除备份文件夹 $this->backFile($this->root_dir,$this->backup_dir,$ex); //执行升级 $upres = $this->execute_update($mfile); //更新版本 //Db::name('system')->update(['sys_version_num'=>$version_num,'id'=>1]); $value = [ 'version' => $version_num ]; $res = SetArr::name('taoler')->edit($value); if($res == false){ return json(['code'=>-1,'msg'=>'代码更新成功,但版本写入失败']); } return json(['code'=>0,'msg'=>'升级成功']); } /** * 备份 * @param string $dir * @param string $backDir * @param array $ex * @return \think\response\Json */ public function backFile(string $dir,string $backDir,array $ex) { $backRes = Files::copydirs($dir, $backDir, $ex); $backData = $backRes->getData(); if($backData['code'] == -1){ Log::channel('update')->info('update:{type} {progress} {msg}',['type'=>'error','progress'=>'25%','msg'=>'备份失败!']); return json(['code'=>-1,'msg'=>$backRes['msg']]); } Log::channel('update')->info('update:{type} {progress} {msg}',['type'=>'success','progress'=>'30%','msg'=>'执行文件备份成功!']); } }