execute($source,$target); print << Скрипт, обновляющий сам себя ({$version})

Обновление скрипта на {$version}

Обновление выполнено успешно

HTML; break; } default: { /* * примитивный функционал скрипта */ #CUSTOM_VERSION_CODE $version = 'V1'; header("Content-type: text/html; charset=utf-8"); $custom = '

V1: Hello World!

'; #CUSTOM_VERSION_CODE print << Скрипт, обновляющий сам себя ({$version}) {$custom}

Обновить скрипт

Выберите версию скрипта для установки:


Выбранный вариант (V1 или V2) будет загружен с сервера webasyst.com и заменит текущую версию скрипта index.php
HTML; break; } } }catch(Exception $ex){ header("Content-type: text/plain; charset=utf-8"); print "\n".str_repeat('==',30)."\n"; print "Exception with code {$ex->getCode()}:\n\t{$ex->getMessage()}\n"; print "\nTrace:\n"; print $ex->getTraceAsString(); } /* -------------------------------------------------------- */ /** * * selfUpdate * * Класс для обновления одного файла файлом из удаленного источника. * Адрес источника передается в конструктор. Скрипт загружает файл во временную папку * */ class selfUpdate { const TIMEOUT_SOCKET = 15; /** * * @var string директория на сервере, куда загружается обновление, чтобы потом заменить работающий скрипт index.php */ const PATH_UPDATE = 'updates/download/'; /** * * @var string директория для хранения прошлой версии скрипта */ const PATH_BACKUP = 'updates/backup/'; /** * * @var string корневая директория установки */ private $root_path; /** * * @var int размер загружаемого файла */ private $content_length = 0; /** * * @var string хеш загружаемого файла */ private $content_md5 = null; /** * * @var int текущий размер загруженного файла */ private $current_content_length = 0; /** * * @var string путь до источника обновлений */ private $update_uri = null; /** * * @var resource */ private $download_stream = null; /** * * @param $root_path * @return Update */ function __construct($root_path = null) { if(!isset(self::$root_path)){ $this->root_path = self::formatPath($root_path?$root_path:dirname(__FILE__)).'/'; } } /** * Основной метод загрузки и установки обновления * * @throws Exception * @param $update_uri string URI источника обновлений * @param $target string путь относительно корня установки к обновляемому файлу * @return array() */ public function execute($update_uri,$target) { try{ $this->update_uri = $update_uri; $target = self::formatPath($target).'/'; $target = preg_replace('@(^|/)\.\./@','/',$target); $env = self::onBeforeUpdate(); $this->cleanupPath(self::PATH_UPDATE); $download_file = $this->download(); $this->replace($download_file,$target); $this->cleanupPath(self::PATH_UPDATE); self::onAfterUpdate($env); }catch(Exception $ex){ //В случае ошибки возвращаем переменные окружения в исходное состояние self::onAfterUpdate($env); //и очищаем директорию от временных файлов $this->cleanupPath(self::PATH_UPDATE,true); throw $ex; } } /** * Подготавливаем окружение к обновлению * @return array() */ private static function onBeforeUpdate() { $env = array(); $env['session_id'] = session_id(); if($env['session_id']){ //KNOWHOW - на длительных операциях открытая сессия блокирует другие скрипты (с одинаковым идентификатором сессии), // вынуждая их ожидать завершения текущего процесса session_write_close(); } //KNOWHOW не даем завершаться скрипту даже если браузер закрыл соединение ignore_user_abort(true); return $env; } /** * Возвращаем окружение в исходное состояние * @param $env array * @return void */ private static function onAfterUpdate($env) { if($env['session_id']){ session_start(); } } /** * Загрузка файла с удаленного сервера. * Определяет подходящий метод загрузки в зависимости от серверного окружения. * * @throws Exception * @return string downloaded file_path */ private function download() { try{ $name = basename(preg_replace('/(\?.*)/','',$this->update_uri)); $download_file = self::formatPath(self::PATH_UPDATE.'/'.$name); $this->download_stream=$this->fopen($download_file,'wb'); if(!$this->download_stream){ throw new Exception("Не могу создать временный файл {$download_file}"); } if($this->curlAvailable()){ //при доступности cURL используем его, так как метод более гибкий $this->downloadCurl(); }elseif($this->fopenAvailable()){ //иначе, если allow_fopen_url = On, пробуем получить обновление через fopen() $this->downloadFopen(); }else{ throw new Exception('Нет подходящего транспорта для установки обновления (не поддерживаются ни cURL, ни allow_fopen_url)'); } if($this->download_stream && is_resource($this->download_stream)){ fclose($this->download_stream); } //проверяем длину ответа от сервера - она может не совпадать с заявленной // - в большую сторону в случае допвывода со стороны сервера ошибок и т.п. // - в меньшую в случае обрыва соединения if($this->content_length && ($real_content_length = filesize($this->root_path.$download_file)) && ($this->content_length != $real_content_length)){ throw new Exception(sprintf("Неверный размер файла. Ожидали %d, а получили %d байт.",$this->content_length,$real_content_length)); } //проверяем md5 хеш загруженного файла if($this->content_md5 && ($md5 = md5_file($this->root_path.$download_file)) && (strcasecmp ($this->content_md5,$md5)!=0)){ throw new Exception(sprintf("Неверная md5-хеш файла. Ожидали %s, а получили %s",$this->content_md5,$md5)); } return $download_file; }catch(Exception $ex){ if($this->download_stream && is_resource($this->download_stream)){ fclose($this->download_stream); } if($download_file && file_exists($this->root_path.$download_file)){ @unlink($this->root_path.$download_file); } throw $ex; } } /** * Загрузка файла с удаленного сервера через fopen() * Для работы необходимо, чтобы в настройках PHP было allow_fopen_url = On * * @return void */ private function downloadFopen() { $source_stream = null; try{ //по умолчанию таймаут на открытие ресурсов составляет 30 - это слишком много, чтобы узнать, что сети нет, поэтому ставим меньший таймаут $default_socket_timeout = ini_set('default_socket_timeout', self::TIMEOUT_SOCKET); $source_stream = fopen($this->update_uri, 'r'); ini_set('default_socket_timeout', $default_socket_timeout); if(!$source_stream){ throw new Exception("Ошибка подключения к источнику обновлений [{$this->update_uri}]."); } $this->getStreamInfo($source_stream); $retry_counter = 0; while( ($delta=stream_copy_to_stream($source_stream,$this->download_stream,102400)) //бывает, что последние байты ответа сервер отдает очень неохотно - nginx и т.п. ||( $this->content_length && ($this->current_content_length<$this->content_length) && (++$retry_counter<20) ) ||( !$this->content_length && (++$retry_counter<3) ) ){ if($delta){ $this->current_content_length += $delta; $retry_counter = 0; }else{ sleep(3); } } fclose($source_stream); }catch(Exception $ex){ if($source_stream && is_resource($source_stream)){ fclose($source_stream); } throw $ex; } } /** * Загрузка файла с удаленного сервера с помощью cURL * * @throws Exception * @return void */ private function downloadCurl() { try{ $ch = null; if (!($ch = curl_init()) ){ throw new Exception('err_curlinit'); } if ( curl_errno($ch) != 0 ){ throw new Exception('err_curlinit'.curl_errno($ch).' '.curl_error($ch)); } $curl_options = array( CURLOPT_HEADER => 0, CURLOPT_RETURNTRANSFER => 1, CURLOPT_TIMEOUT => self::TIMEOUT_SOCKET*60, CURLOPT_CONNECTTIMEOUT => self::TIMEOUT_SOCKET, CURLE_OPERATION_TIMEOUTED => self::TIMEOUT_SOCKET*60, CURLOPT_BINARYTRANSFER => true, //KNOWHOW переопределенная функция записи позволяет дополнительно фиксировать информацию о переданном размере CURLOPT_WRITEFUNCTION => array(&$this,'curlWriteHandler'), //KNOWHOW добавляем хук для чтения заголовков, чтобы узнать размер передаваемого файла и его md5 хеш CURLOPT_HEADERFUNCTION => array(&$this,'curlHeaderHandler'), CURLOPT_URL => $this->update_uri, //TODO на ряде хостингов curl работает только через прокси, который необходимо указать в настройках ); foreach($curl_options as $param=>$option){ curl_setopt($ch, $param, $option); } $res = curl_exec($ch); if ($errno = curl_errno($ch)) { $message = "Curl error: {$errno}# ".curl_error($ch)." at [{$this->update_uri}]"; throw new Exception($message); } $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); if($status != 200){ throw new Exception("Неверный ответ сервера {$this->update_uri}",$status); } curl_close($ch); }catch(Exception $ex) { if($ch){ curl_close($ch); } throw $ex; } } /** * обработчик чтения заголовков для curl * * @param $ch * @param $header * @return int */ private function curlHeaderHandler($ch,$header) { $header_matches = null; if(preg_match('/content-length:\s*(\d+)/i',$header,$header_matches)) { $this->content_length = intval($header_matches[1]); }elseif(preg_match('/content-md5:\s*([\da-f]{32})/i',$header,$header_matches)){ $this->content_md5=$header_matches[1]; } return strlen($header); } /** * обработчик записи в файл для curl * * @param $ch * @param $chunk * @return int */ private function curlWriteHandler($ch,$chunk) { $size = 0; if($this->download_stream&&is_resource($this->download_stream)) { $size = fwrite($this->download_stream,$chunk); $this->current_content_length += $size; }else { throw new Exception('Ошибка сохранения файла на сервере'); } return $size; } /** * Replace current version by merged old and updated files * @param $source_path string * @param $target_path string * @return string backup directory paty */ private function replace($source_path,$target_path) { $target_path = self::formatPath($target_path); $source_path = self::formatPath($source_path); $backup_path = false; if(file_exists($this->root_path.$target_path)){ $backup_path = self::PATH_BACKUP; $backup_path = self::formatPath($backup_path); $this->cleanupPath($backup_path); $this->mkdir($backup_path); $backup_path .= '/'.basename($target_path); } if($backup_path){ if(!$this->rename($target_path,$backup_path)){ throw new Exception("Ошибка создания бекапа {$target_path} в папке {$backup_path}"); } } if(!$this->rename($source_path,$target_path)){ //rollback rename if($backup_path){ $this->rename($backup_path,$target_path); } throw new Exception("Ошибка обновления {$target_path} в {$source_path}"); } return $backup_path; } /** * "Настойчивое" переименование * * @param $oldname string path * @param $newname string path * @return boolean */ private function rename($oldname,$newname) { $result = false; if(@rename($this->root_path.$oldname,$this->root_path.$newname) ||sleep(3) ||@rename($this->root_path.$oldname,$this->root_path.$newname)){ $result = true; } return $result; } /** * Очистка директории от файлов * * @param $paths string|array * @param $skip_directory можно оставлять неудаляемые директории * @return void */ private function cleanupPath($paths,$skip_directory = false) { foreach((array)$paths as $path){ try{ if(file_exists($this->root_path.$path)){ $dir=opendir($this->root_path.$path); while (false!==($current_path=readdir($dir))){ if(($current_path != '.' )&&($current_path != '..')){ if(is_dir($this->root_path.$path.'/'.$current_path)){ $this->cleanupPath($path.'/'.$current_path,$skip_directory); }else{ if(!@unlink($this->root_path.$path.'/'.$current_path)){ throw new Exception("Не могу удалить файл {$path}/{$current_path}"); } } } } closedir($dir); if(!@rmdir($this->root_path.$path)&&!$skip_directory){ throw new Exception("Не могу удалить директорию {$path}"); } } }catch(Exception $ex){ if($dir&&is_resource($dir)){ closedir($dir); } throw $ex; } } } /** * Приводим пути к nix виду * * windows поймет и такие, а в случае использования правил постобработки с использованием регулярных выражения последние упрощаются * @param $path string * @return string */ private static function formatPath($path) { $path = preg_replace('@([/\\\\]+)@','/',$path); return preg_replace('@/$@','',$path); } /** * Создание директорий с дополнительными проверками на права записи * * @param $target_path * @param $mode * @return void */ private function mkdir($target_path,$mode = 0777) { if(!file_exists($this->root_path.$target_path)){ if(!mkdir($this->root_path.$target_path,$mode&0777,true)){ throw new Exception("не могу создать директорию {$target_path}"); } }elseif(!is_dir($this->root_path.$target_path)){ throw new Exception("Не могу создать директорию {$target_path}, так как есть файл с таким изменем"); }elseif(!is_writable($this->root_path.$target_path)){ throw new Exception("{$target_path} должна быть доступна по записи. Установите необходимые права доступа."); } } /** * Проверяем возможность использовать cURL * * @return boolean */ private function curlAvailable() { return extension_loaded('curl') && function_exists('curl_init') && preg_match('/https?:\/\//',$this->update_uri); } /** * Проверяем возможность использовать fopen * * @return boolean */ private function fopenAvailable() { $result = false; if(stream_is_local($this->update_uri)){ $result = true; }else{ $scheme = parse_url($this->update_uri,PHP_URL_SCHEME); if($scheme == 'https'){ $scheme = 'http'; } $result = ini_get('allow_url_fopen') && in_array($scheme,stream_get_wrappers()); } return $result; } /** * Читаем метаданные загружаемого файла * * @param $source_stream resource * @param $download_content_length int * @return void */ private function getStreamInfo($source_stream,$download_content_length=4096) { $stream_meta_data=stream_get_meta_data($source_stream); //KNOWHOW без явного чтения потока метаданные потока не всегда доступны //read data chunk to determine stream meta data $buf = stream_get_contents($source_stream,$download_content_length); $this->current_content_length = min($download_content_length,strlen($buf)); $stream_seekable = isset($stream_meta_data['seekable'])?$stream_meta_data['seekable']:false; $headers = array(); //В зависимости от реализации обертки для http заголовки могут находиться в разных местах if(isset($stream_meta_data["wrapper_data"]["headers"])){ $headers = $stream_meta_data["wrapper_data"]["headers"]; }elseif(isset($stream_meta_data["wrapper_data"])){ $headers = $stream_meta_data["wrapper_data"]; } $header_matches = null; foreach($headers as $header){ //ищем информацию о размере передаваемых данных if(preg_match('/content-length:\s*(\d+)/i',$header,$header_matches)){ $this->content_length=intval($header_matches[1]); //и md5 хеше }elseif(preg_match('/content-md5:\s*([\da-f]{32})/i',$header,$header_matches)){ $this->content_md5=$header_matches[1]; } } if($buf && $this->download_stream){ fwrite($this->download_stream,$buf); } } /** * "Настойчивое" открытие файла * на случай если ресурс занят другим процессом или директория еще не создана * @param $filename * @param $mode * @param $retry * @return resource */ private function fopen($filename,$mode,$retry = 5) { $path = $this->root_path.$filename; if(!file_exists($path)){ $this->mkdir(dirname($filename)); } while(!($fp = fopen($path,$mode))){ if(--$retry>0){ sleep(1); }else{ break; } } return $fp; } } //EOF