php 在线升级 功能
目前已修改为通用解决方案 点击下方链接下载 此为最新优化过的版本 之后的贴图 说明 仅用来做思路参考 我就不再修改了
下面是思路解析
有很久没有写博客了 最近这家公司让做一个类似cms框架的在线自动升级功能,参照了一下织梦的升级流程 自己使用TP5编写了一套程序 可能不是最优方案,但是功能实现了,以下是两种实现思路 由于时间紧迫本人选择了第一种并进行了流程简化 并且附上了免费的程序资源 可自行下载本地测试 sql文件在上方 最新压缩包 优化为通用解决方案 由于已上传服务器 内容做了一些更改 下面的代码部分就不再变动了 请参考程序压缩包内的内容 目前已经取消了对本地数据库的查询 直接通过json文件操作
在此友情提醒 做后端千万不要相信客户会正常操作不要相信前端会帮你进行参数和各种情况的判断处理,既然后端负责逻辑处理就要做到滴水不漏,考虑到各种情况 避免因小失大 导致整个服务器挂掉
思路一
1: 更新服务器指定目录需要一个每次升级的版本记录文件
例如格式: 更新日期,字符集,版本号,更新标题,更新文件的压缩包(压缩包内是更新需要的文件)
20140415,V5.7.41,20140415常规更新补丁,http://upgrade.diyi01.com/upgrade/upgrade-20140415.zip
2: 获取客户网站的最近更新日期,该日期可存于数据库或者文件 建议存储于文件 尽量避免对数据库的访问
3: 读取远程更新服务器更新文件信息,然后比对客户网站需要更新那些压缩包
4: 读取远程压缩包的配置文件(该文件记录压缩包内的文件列表),并根据配置文件判断是否客户网站是否有写入权限,
如:
下载的文件临时存放在文件夹(…/data/20140415)内,如果某些文件自己有改动导致更新中途中错,您可以从这文件夹提取文件手工更新。
本次升级需要在下面文件夹写入更新文件,请注意文件夹是否有写入权限:
…/lib/ 状态:[√正常]
…/lib/Article/ 状态:[√正常]
…/Public/ 状态:[√正常]
5: 下载更新的文件到临时目录(可以直接下载压缩包在解压出文件)下载过程需要显示文件是否下载ok,完成提示安装更新按钮
6: 点击更新首先判断是否有sql文件,有先更新sql,在更新文件
7: 更新完成改写客户网站更新时间,并删除下载的临时更新文件
思路二
写两个程序,一个是主程序;一个是升级程序;所有升级任务都由升级程序完成。
1.启动升级程序,升级程序连接到网站,下载新的主程序(当然还包括支持的库文件、XML配置文档等)到临时文件夹;
2.升级程序获取服务器端XML配置文件中新版本程序的更新日期或版本号或文件大小;
3.升级程序获取原有客户端应用程序的最近一次更新日期或版本号或文件大小,两者进行比较;如果发现升级程序的日期大于原有程序的最新日期,则提示用户是否升级;或者是采用将现有版本与最新版本作比较,发现最新的则提示用户是否升级;也有人用其它属性如文件大小进行比较,发现升级程序的文件大小大于旧版本的程序的大小则提示用户升级。本文主要采用比较新旧版本更新日期号来提示用户升级。
4.如果用户选择升级,则获取升级程序压缩包,开始进行下载;
5.升级程序检测旧的主程序是否活动,若活动则关闭旧的主程序;
6.删除旧的主程序,拷贝临时文件夹中的文件到相应的位置;
7.检查主程序的状态,若状态为活动的,则启动新的主程序;
8.关闭升级程序,升级完成
请注意,以下代码是一个已经在本地phpstudy 实现功能的demo,里面很多参数需要大家自行根据情况修改,但是整体思路和流程没有问题,其中引用的model是一个空模型 只是为了控制器操作数据库 测试方法就是直接在WWW目录创建两个项目类似这种结构
升级程序
<?php
namespace app\system\controller;
use think\Controller;
use think\Db;
use app\common\model\Info;
class Upgrade extends Controller
{
public function index()
{
return 'connected';
}
// 外部请求
public function dataRequest($url,$https=true,$method='get',$data=null){
if (trim($url) == '') {
return false;
}
//初始化curl
$ch = curl_init($url);
//字符串不直接输出,进行一个变量的存储
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
//https请求
if ($https === true) {
//确保https请求能够请求成功
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,false);
curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,false);
}
//post请求
if ($method == 'post') {
curl_setopt($ch,CURLOPT_POST,true);
curl_setopt($ch,CURLOPT_POSTFIELDS,$data);
}
//发送请求
$str = curl_exec($ch);
$aStatus = curl_getinfo($ch);
//关闭连接
curl_close($ch);
if(intval($aStatus["http_code"])==200){
// json数据处理
return json_decode($str);
// return $str;
}else{
return false;
}
}
// 检测是否有新版本
public function check_version(){
// 打开远程版本记录文件比对本地记录文件
// 设定目录
$server_dir = 'http://192.168.31.64/tp5/public/update/up_log.txt';
$local_dir = ROOT_PATH . 'public/update/ver.txt';
// 获取版本记录文件
$server = $this->get_file($server_dir);
if ($server === false) {
$result= [
'code'=>406,
'msg'=>'服务器版本记录文件获取失败',
'data'=>''
];
} else {
// 最新版本
$server = explode(",", $server);
$last_version = end($server);
// 本地版本
$local = $this->get_file($local_dir);
if ($local === false) {
$result= [
'code'=>406,
'msg'=>'本地版本记录文件获取失败',
'data'=>''
];
} else {
// 比较版本
$data = [
'last_version' =>$last_version,
];
if (intval($last_version) > intval($local)) {
$result= [
'code'=>200,
'msg'=>'服务器有新版本',
'data'=>$data
];
} else {
$result= [
'code'=>204,
'msg'=>'已经是最新版本',
'data'=>$data
];
}
}
}
return json($result);
}
/**
* 解压缩
* @param $file 要解压的文件
* @param $todir 要存放的目录
* @return str 包含所有文件及目录的数组
*/
public function deal_zip($file,$todir)
{
if (trim($file) == '') {
return 406;
}
if (trim($todir) == '') {
return 406;
}
$zip = new \ZipArchive;
// 中文文件名要使用ANSI编码的文件格式
if ($zip->open($file) === TRUE) {
//提取全部文件
$zip->extractTo($todir);
$zip->close();
$result = 200;
} else {
$result = 406;
}
return $result;
}
/**
* 遍历当前目录不包含下级目录
* @param $dir 要遍历的目录
* @param $file 要过滤的文件
* @return str 包含所有文件及目录的数组
*/
public function scan_dir($dir,$file='')
{
if (trim($dir) == '') {
return false;
}
$file_arr = scandir($dir);
$new_arr = [];
foreach($file_arr as $item){
if($item!=".." && $item !="." && $item != $file){
$new_arr[] = $item;
}
}
return $new_arr;
}
/**
* 合并目录且只覆盖不一致的文件
* @param $source 要合并的文件夹
* @param $target 要合并的目的地
* @return int 处理的文件数
*/
public function copy_merge($source, $target) {
if (trim($source) == '') {
return false;
}
if (trim($target) == '') {
return false;
}
// 路径处理
$source = preg_replace ( '#/\\\\#', DIRECTORY_SEPARATOR, $source );
$target = preg_replace ( '#\/#', DIRECTORY_SEPARATOR, $target );
$source = rtrim ( $source, DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR;
$target = rtrim ( $target, DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR;
// 记录处理了多少文件
$count = 0;
// 如果目标目录不存在,则创建。
if (! is_dir ( $target )) {
mkdir ( $target, 0777, true );
$count ++;
}
// 搜索目录下的所有文件
foreach ( glob ( $source . '*' ) as $filename ) {
if (is_dir ( $filename )) {
// 如果是目录,递归合并子目录下的文件。
$count += $this->copy_merge ( $filename, $target . basename ( $filename ) );
} elseif (is_file ( $filename )) {
// 如果是文件,判断当前文件与目标文件是否一样,不一样则拷贝覆盖。
// 这里使用的是文件md5进行的一致性判断,可靠但性能低。
if (! file_exists ( $target . basename ( $filename ) ) || md5 ( file_get_contents ( $filename ) ) != md5 ( file_get_contents ( $target . basename ( $filename ) ) )) {
copy ( $filename, $target . basename ( $filename ) );
$count ++;
}
}
}
// 返回处理了多少个文件
return $count;
}
/**
* 遍历删除文件
* @param $dir 要删除的目录
* @return bool 成功与否
*/
public function deldir($dir) {
if (trim($dir) == '') {
return false;
}
//先删除目录下的文件:
$dh=opendir($dir);
while ($file=readdir($dh)) {
if($file!="." && $file!="..") {
$fullpath=$dir."/".$file;
if(!is_dir($fullpath)) {
unlink($fullpath);
} else {
$this-> deldir($fullpath);
}
}
}
closedir($dh);
//删除当前文件夹:
if(rmdir($dir)) {
return true;
} else {
return false;
}
}
/**
* 遍历执行sql文件
* @param $dir 要执行的目录
* @return bool 成功与否
*/
public function carry_sql($dir){
if (trim($dir) == '') {
return false;
}
$sql_file_res = $this->scan_dir($dir);
if (!empty($sql_file_res)) {
foreach ($sql_file_res as $k => $v) {
if (!empty(strstr($v,'.sql'))) {
$sql_content = file_get_contents($dir.$v);
$sql_arr = explode(';', $sql_content);
//执行sql语句
foreach ($sql_arr as $vv) {
if (!empty($vv)) {
$sql_res = Db::execute($vv.';');
if (empty($sql_res)) {
return false;
}
}
}
}
}
} else {
return false;
}
return true;
}
/**
* 下载程序压缩包文件
* @param $url 要下载的url
* @param $save_dir 要存放的目录
* @return res 成功返回下载信息 失败返回false
*/
function down_file($url, $save_dir) {
if (trim($url) == '') {
return false;
}
if (trim($save_dir) == '') {
return false;
}
if (0 !== strrpos($save_dir, '/')) {
$save_dir.= '/';
}
$filename = basename($url);
//创建保存目录
if (!file_exists($save_dir) && !mkdir($save_dir, 0777, true)) {
return false;
}
//开始下载
$ch = curl_init();
$timeout = 5;
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
$content = curl_exec($ch);
$status = curl_getinfo($ch);
curl_close($ch);
// 判断执行结果
if ($status['http_code'] ==200) {
$size = strlen($content);
//文件大小
$fp2 = @fopen($save_dir . $filename , 'a');
fwrite($fp2, $content);
fclose($fp2);
unset($content, $url);
$res = [
'status' =>$status['http_code'] ,
'file_name' => $filename,
'save_path' => $save_dir . $filename
];
} else {
$res = false;
}
return $res;
}
/**
* 获取文件内容
* @param $url 要获取的url
* @return res 成功返回内容 失败返回false
*/
public function get_file($url){
if (trim($url) == '') {
return false;
}
$opts = array(
'http'=>array(
'method'=>"GET",
'timeout'=>3,//单位秒
)
);
$cnt=0;
while($cnt<3 && ($res=@file_get_contents($url, false, stream_context_create($opts)))===FALSE) $cnt++;
if ($res === false) {
return false;
} else {
return $res;
}
}
// 在线更新
public function system_update(){
// 有效期核验
// 查询用户身份标识并请求接口
$Info = new Info();
$sn = $Info
->where('id', 1)
->field('sn')
->find();
$data = [
'sn'=>$sn->sn
];
// 访问服务器判断有效期:
$res = $this -> dataRequest('http://192.168.31.64/tp5/public/update/server/check_date',false,'post',$data);
if ($res->data === false) {
// 不在有效期
$result = [
'code'=>401,
'msg'=>'已过服务有效期',
'data'=>''
];
} else {
// 有效期内 开始更新
// 设定目录
// 根目录
$base_dir = ROOT_PATH;
// 服务器更新路径
$update_res = 'http://192.168.31.64/tp5/public/update/';
// 本地更新路径
$local_up_dir = $base_dir.'public/update/';
// 本地缓存路径
$path = $base_dir . 'public\update\cache';
// 没有就创建
if(!is_dir($path)){
mkdir(iconv("UTF-8", "GBK", $path),0777,true);
}
// 设定缓存目录名称
$cache_dir = $path.'\\';
// 看看需要下载几个版本的压缩包
// 服务器更新日志存放路径
$server = $this->get_file($update_res.'up_log.txt');
if ($server === false) {
$result = [
'code'=>406,
'msg'=>'服务器更新日志获取失败',
'data'=>''
];
}else{
// 版本记录
$server = explode(",", $server);
$local = $this->get_file($local_up_dir.'ver.txt');
if ($local === false) {
$result = [
'code'=>406,
'msg'=>'本地更新日志获取失败',
'data'=>''
];
} else {
// 循环比较是否需要下载 更新
foreach ($server as $key => $value) {
if ($local < $value) {
// 获取更新信息
// 服务器各个程序包日志存放路径
$up_info = $this->get_file($update_res.$value.'/version.txt');
// 判断是否存在
if ($up_info === false) {
$result = [
'code'=>406,
'msg'=>'服务器更新包不存在',
'data'=>''
];
} else {
// 信息以json格式存储便于增减和取值 故解析json对象
$up_info = json_decode($up_info);
// 下载文件
$back = $this->down_file($up_info->download,$cache_dir);
if (empty($back)) {
$result = [
'code'=>406,
'msg'=>'升级程序包下载失败',
'data'=>''
];
} else {
//下载成功 解压缩
$zip_res = $this->deal_zip($back['save_path'] ,$cache_dir);
// 判断解压是否成功
if ($zip_res == 406) {
$result = [
'code'=>406,
'msg'=>'文件解压缩失败',
'data'=>''
];
} else {
// 开始更新数据库和文件
// sql文件
//读取文件内容遍历执行sql
$sql_res = $this->carry_sql($cache_dir.'mysql\\');
if ($sql_res === false) {
$result = [
'code'=>406,
'msg'=>'sql文件写入失败',
'data'=>''
];
} else {
// php文件合并 返回处理的文件数
$file_up_res = $this->copy_merge($cache_dir.'program\\',$base_dir);
if (empty($file_up_res)) {
$result = [
'code'=>406,
'msg'=>'文件移动合并失败',
'data'=>''
];
}else{
// 更新完改写网站本地版号
$write_res = file_put_contents($local_up_dir . 'ver.txt', $value);
if (empty($write_res)) {
$result = [
'code'=>406,
'msg'=>'本地更新日志改写失败',
'data'=>''
];
}else{
// 删除临时文件
$del_res = $this->deldir($cache_dir);
if (empty($del_res)) {
$result = [
'code'=>406,
'msg'=>'更新缓存文件删除失败',
'data'=>''
];
}else{
$result = [
'code'=>200,
'msg'=>'在线升级已完成',
'data'=>''
];
}
}
}
}
}
}
}
}else{
$result = [
'code'=>406,
'msg'=>'本地已经是最新版',
'data'=>''
];
}
}
}
}
}
return json($result);
}
}
服务器检测版本
<?php
namespace app\update\controller;
use think\controller;
use app\common\model\Sn;
class Server extends \think\Controller
{
// 有效期判断
public function check_date()
{
$data = input('post.sn','','trim');
if (empty($data)) {
$result = [
'code' =>406,
'msg' =>'未接收到身份识别码',
'data' =>true
];
}else{
// 查询 身份是否过有效期
$Sn = new Sn;
$res = $Sn
->where('sn',$data)
->find();
$now_time = time();
if ($res->over_date >= $now_time) {
// 未过有效期
$result = [
'code' =>204,
'msg' =>'很高兴为您服务',
'data' =>true
];
} else {
// 已过有效期
$result = [
'code' =>416,
'msg' =>'对不起,已过服务有效期',
'data' =>false
];
}
}
return json($result);
}
}
以下是支撑这个功能的一些文件格式 文件夹格式 大家可以自行本地创建修改 但是要对应升级程序中的命名避免报错 需要注意的是txt文件一定要用编辑器去写内容 保证是utf8无bom格式 否则容易出错还很难察觉
以下是文件内容,仅记录了版号用于比对版本判断是否需要更新
接下来是服务器端文件 这里比较重要
up_log内容记录了每次服务器更新的版本号
用于本地跨版本升级时把之前没有升级的版本更新都补上
升级文件夹内部包含了本次版本记录信息和程序压缩包
这里需要注意的是我的记录文件使用的是json格式 便于后期增减参数 升级程序只需要格式化json数据后读取特定键值即可 大家可以自行使用json_encode 函数直接对数组进行格式化后存储进去
压缩包包含了 .sql文件和主程序
sql文件中的内容是使用 navicat for mysql软件增加字段后 直接预览 sql语句 复制的 说白了数据库这块的升级无非也就是增加表字段 本来预想备份数据库数据之后重新创建与远端服务器一样的表再插入现有备份的表数据 但是考虑到安全性以及避免升级过程中出错导致整个本地服务器挂掉 选择了更稳妥的办法
其中的主程序就是直接把 有更改的文件 从程序根目录开始进行了添加 这里大家可以直接把整个根目录下的文件都做成program 因为我在更新覆盖时进行的是文件md5校验 没有变更的文件是不会被更新的