diff --git a/README.md b/README.md index d900f94..c6589ad 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,25 @@ of your software. Just make sure you set the proper repository in your `config/self-updater.php` file. +### Using Http archives +The package comes with an _Http_ source repository type to fetch +releases from an HTTP directory listing containing zip archives. + +To run with HTTP archives, use following settings in your `.env` file: + +| Config name | Value / Description | +| ----------- | ----------- | +| SELF_UPDATER_SOURCE | `http` | +| SELF_UPDATER_REPO_URL | Archive URL, e.g. `http://archive.webapp/` | +| SELF_UPDATER_PKG_FILENAME_FORMAT | Zip package filename format | +| SELF_UPDATER_DOWNLOAD_PATH | Download path on the webapp host server| + +The archive URL should contain nothing more than a simple directory listing with corresponding zip-Archives. + +`SELF_UPDATER_PKG_FILENAME_FORMAT` contains the filename format for all webapp update packages. I.e. when the update packages listed on the archive URL contain names like `webapp-v1.2.0.zip`, `webapp-v1.3.5.zip`, ... then the format should be `webapp-v_VERSION_`. The `_VERSION_` part is used as semantic versionioning variable for `MAJOR.MINOR.PATCH` versioning. The zip-extension is automatically added. + +The target archive files must be zip archives and should contain all files on root level, not within an additional folder named like the archive itself. + ## Extending and adding new source repository types You want to pull your new versions from elsewhere? Feel free to create your own source repository type somewhere but keep in mind for the new diff --git a/config/self-update.php b/config/self-update.php index a963882..07a629b 100644 --- a/config/self-update.php +++ b/config/self-update.php @@ -32,6 +32,7 @@ | A repository can be of different types, which can be specified here. | Current options: | - github + | - http | */ @@ -43,6 +44,12 @@ 'repository_url' => '', 'download_path' => env('SELF_UPDATER_DOWNLOAD_PATH', '/tmp'), ], + 'http' => [ + 'type' => 'http', + 'repository_url' => env('SELF_UPDATER_REPO_URL', ''), + 'pkg_filename_format' => env('SELF_UPDATER_PKG_FILENAME_FORMAT', 'v_VERSION_'), + 'download_path' => env('SELF_UPDATER_DOWNLOAD_PATH', '/tmp'), + ], ], /* diff --git a/src/Listeners/SendUpdateAvailableNotification.php b/src/Listeners/SendUpdateAvailableNotification.php index 63a77f5..c58b80d 100644 --- a/src/Listeners/SendUpdateAvailableNotification.php +++ b/src/Listeners/SendUpdateAvailableNotification.php @@ -2,8 +2,8 @@ namespace Codedge\Updater\Listeners; -use Illuminate\Log\Writer; use Illuminate\Mail\Mailer; +use Illuminate\Support\Facades\Log; use Codedge\Updater\Events\UpdateAvailable; /** @@ -14,11 +14,6 @@ */ class SendUpdateAvailableNotification { - /** - * @var \Monolog\Logger - */ - protected $logger; - /** * @var Mailer */ @@ -27,12 +22,10 @@ class SendUpdateAvailableNotification /** * SendUpdateAvailableNotification constructor. * - * @param Writer $logger * @param Mailer $mailer */ - public function __construct(Writer $logger, Mailer $mailer) + public function __construct(Mailer $mailer) { - $this->logger = $logger->getMonolog(); $this->mailer = $mailer; } @@ -44,7 +37,7 @@ public function __construct(Writer $logger, Mailer $mailer) public function handle(UpdateAvailable $event) { if (config('self-update.log_events')) { - $this->logger->addInfo('['.$event->getEventName().'] event: Notification triggered.'); + Log::info('['.$event->getEventName().'] event: Notification triggered.'); } $sendToAddress = config('self-update.mail_to.address'); @@ -52,14 +45,14 @@ public function handle(UpdateAvailable $event) $subject = config('self-update.mail_to.subject_update_available'); if (empty($sendToAddress)) { - $this->logger->addCritical( + Log::critical( '['.$event->getEventName().'] event: ' .'Missing recipient email address. Please set SELF_UPDATER_MAILTO_ADDRESS in your .env file.' ); } if (empty($sendToName)) { - $this->logger->addWarning( + Log::warning( '['.$event->getEventName().'] event: ' .'Missing recipient email name. Please set SELF_UPDATER_MAILTO_NAME in your .env file.' ); diff --git a/src/Listeners/SendUpdateSucceededNotification.php b/src/Listeners/SendUpdateSucceededNotification.php index 573ee41..74bd873 100644 --- a/src/Listeners/SendUpdateSucceededNotification.php +++ b/src/Listeners/SendUpdateSucceededNotification.php @@ -2,8 +2,8 @@ namespace Codedge\Updater\Listeners; -use Illuminate\Log\Writer; use Illuminate\Mail\Mailer; +use Illuminate\Support\Facades\Log; use Codedge\Updater\Events\UpdateSucceeded; /** @@ -14,11 +14,6 @@ */ class SendUpdateSucceededNotification { - /** - * @var \Monolog\Logger - */ - protected $logger; - /** * @var Mailer */ @@ -27,12 +22,10 @@ class SendUpdateSucceededNotification /** * SendUpdateAvailableNotification constructor. * - * @param Writer $logger * @param Mailer $mailer */ - public function __construct(Writer $logger, Mailer $mailer) + public function __construct(Mailer $mailer) { - $this->logger = $logger->getMonolog(); $this->mailer = $mailer; } @@ -44,7 +37,7 @@ public function __construct(Writer $logger, Mailer $mailer) public function handle(UpdateSucceeded $event) { if (config('self-update.log_events')) { - $this->logger->addInfo('['.$event->getEventName().'] event: Notification triggered.'); + Log::info('['.$event->getEventName().'] event: Notification triggered.'); } $sendToAddress = config('self-update.mail_to.address'); @@ -52,14 +45,14 @@ public function handle(UpdateSucceeded $event) $subject = config('self-update.mail_to.subject_update_succeeded'); if (empty($sendToAddress)) { - $this->logger->addCritical( + Log::critical( '['.$event->getEventName().'] event: ' .'Missing recipient email address. Please set SELF_UPDATER_MAILTO_ADDRESS in your .env file.' ); } if (empty($sendToName)) { - $this->logger->addWarning( + Log::warning( '['.$event->getEventName().'] event: ' .'Missing recipient email name. Please set SELF_UPDATER_MAILTO_NAME in your .env file.' ); @@ -68,7 +61,7 @@ public function handle(UpdateSucceeded $event) $this->mailer->send( 'vendor.self-update.mails.update-available', [ - 'newVersion' => $event->getVersionAvailable(), + 'newVersion' => $event->getVersionUpdatedTo(), ], function ($m) use ($subject, $sendToAddress, $sendToName) { $m->subject($subject); diff --git a/src/SourceRepositoryTypes/HttpRepositoryType.php b/src/SourceRepositoryTypes/HttpRepositoryType.php new file mode 100644 index 0000000..6c38181 --- /dev/null +++ b/src/SourceRepositoryTypes/HttpRepositoryType.php @@ -0,0 +1,296 @@ + + * @copyright See LICENSE file that was distributed with this source code. + */ +class HttpRepositoryType extends AbstractRepositoryType implements SourceRepositoryTypeContract +{ + const NEW_VERSION_FILE = 'self-updater-new-version'; + + /** + * @var Client + */ + protected $client; + + /** + * @var Version prepand string + */ + protected $prepend; + + /** + * @var Version append string + */ + protected $append; + + /** + * Github constructor. + * + * @param Client $client + * @param array $config + */ + public function __construct(Client $client, array $config) + { + $this->client = $client; + $this->config = $config; + $this->config['version_installed'] = config('self-update.version_installed'); + $this->config['exclude_folders'] = config('self-update.exclude_folders'); + // Get prepend and append strings + $this->prepend = preg_replace('/_VERSION_.*$/', '', $this->config['pkg_filename_format']); + $this->append = preg_replace('/^.*_VERSION_/', '', $this->config['pkg_filename_format']); + } + + /** + * Check repository if a newer version than the installed one is available. + * + * @param string $currentVersion + * + * @throws \InvalidArgumentException + * @throws \Exception + * + * @return bool + */ + public function isNewVersionAvailable($currentVersion = '') : bool + { + $version = $currentVersion ?: $this->getVersionInstalled(); + + if (! $version) { + throw new \InvalidArgumentException('No currently installed version specified.'); + } + + // Remove the version file to forcefully update current version + $this->deleteVersionFile(); + + if (version_compare($version, $this->getVersionAvailable(), '<')) { + if (! $this->versionFileExists()) { + $this->setVersionFile($this->getVersionAvailable()); + event(new UpdateAvailable($this->getVersionAvailable())); + } + + return true; + } + + return false; + } + + /** + * Fetches the latest version. If you do not want the latest version, specify one and pass it. + * + * @param string $version + * + * @throws \Exception + * + * @return mixed + */ + public function fetch($version = '') + { + if (($releaseCollection = $this->getPackageReleases())->isEmpty()) { + throw new \Exception('Cannot find a release to update. Please check the repository you\'re pulling from'); + } + + $release = $releaseCollection->first(); + $storagePath = $this->config['download_path']; + + if (! File::exists($storagePath)) { + File::makeDirectory($storagePath, 493, true, true); + } + + if (! $version) { + $release = $releaseCollection->where('name', $version)->first(); + if (! $release) { + throw new \Exception('Given version was not found in release list.'); + } + } + + $versionName = $this->prepend.$release->name.$this->append; + $storageFilename = $versionName.'.zip'; + + if (! $this->isSourceAlreadyFetched($release->name)) { + $storageFile = $storagePath.'/'.$storageFilename; + $this->downloadRelease($this->client, $release->zipball_url, $storageFile); + $this->unzipArchive($storageFile, $storagePath.'/'.$versionName); + } + } + + /** + * Perform the actual update process. + * + * @param string $version + * + * @return bool + */ + public function update($version = '') : bool + { + $this->setPathToUpdate(base_path(), $this->config['exclude_folders']); + + if ($this->hasCorrectPermissionForUpdate()) { + if (empty($version)) { + $version = $this->getVersionAvailable(); + } + $sourcePath = $this->config['download_path'].DIRECTORY_SEPARATOR.$this->prepend.$version.$this->append; + + // Move all directories first + collect((new Finder())->in($sourcePath)->exclude($this->config['exclude_folders'])->directories()->sort(function ($a, $b) { + return strlen($b->getRealpath()) - strlen($a->getRealpath()); + }))->each(function ($directory) { /** @var \SplFileInfo $directory */ + if (count(array_intersect(File::directories( + $directory->getRealPath()), $this->config['exclude_folders'])) == 0) { + File::copyDirectory( + $directory->getRealPath(), + base_path($directory->getRelativePath()).'/'.$directory->getBasename() + ); + } + File::deleteDirectory($directory->getRealPath()); + }); + + // Now move all the files left in the main directory + collect(File::allFiles($sourcePath, true))->each(function ($file) { /* @var \SplFileInfo $file */ + File::copy($file->getRealPath(), base_path($file->getFilename())); + }); + + File::deleteDirectory($sourcePath); + $this->deleteVersionFile(); + event(new UpdateSucceeded($version)); + + return true; + } + + event(new UpdateFailed($this)); + + return false; + } + + /** + * Get the version that is currenly installed. + * Example: 1.1.0 or v1.1.0 or "1.1.0 version". + * + * @param string $prepend + * @param string $append + * + * @return string + */ + public function getVersionInstalled($prepend = '', $append = '') : string + { + return $this->config['version_installed']; + } + + /** + * Get the latest version that has been published in a certain repository. + * Example: 2.6.5 or v2.6.5. + * + * @param string $prepend Prepend a string to the latest version + * @param string $append Append a string to the latest version + * + * @return string + */ + public function getVersionAvailable($prepend = '', $append = '') : string + { + $version = ''; + if ($this->versionFileExists()) { + $version = $this->getVersionFile(); + } else { + $releaseCollection = $this->getPackageReleases(); + if ($releaseCollection->isEmpty()) { + throw new \Exception('Retrieved version list is empty.'); + } + $version = $releaseCollection->first()->name; + } + + return $version; + } + + /** + * Retrieve html body with list of all releases from archive URL. + * + * @throws \Exception + * + * @return mixed|\Psr\Http\Message\ResponseInterface + */ + protected function getPackageReleases() + { + if (empty($url = $this->config['repository_url'])) { + throw new \Exception('No repository specified. Please enter a valid URL in your config.'); + } + + $format = str_replace('_VERSION_', '\d+\.\d+\.\d+', + str_replace('.', '\.', $this->config['pkg_filename_format']) + ).'.zip'; + $count = preg_match_all( + "/($format)<\/a>/i", + $this->client->get($url)->getBody()->getContents(), + $files); + $collection = []; + $url = preg_replace('/\/$/', '', $url); + for ($i = 0; $i < $count; $i++) { + $basename = preg_replace("/^$this->prepend/", '', + preg_replace("/$this->append$/", '', + preg_replace('/.zip$/', '', $files[1][$i]) + )); + array_push($collection, (object) [ + 'name' => $basename, + 'zipball_url' => $url.'/'.$files[1][$i], + ]); + } + // Sort collection alphabetically descending to have newest package as first + array_multisort($collection, SORT_DESC); + + return new Collection($collection); + } + + /** + * Check if the file with the new version already exists. + * + * @return bool + */ + protected function versionFileExists() : bool + { + return Storage::exists(static::NEW_VERSION_FILE); + } + + /** + * Write the version file. + * + * @param $content + * + * @return bool + */ + protected function setVersionFile(string $content) : bool + { + return Storage::put(static::NEW_VERSION_FILE, $content); + } + + /** + * Get the content of the version file. + * + * @return string + */ + protected function getVersionFile() : string + { + return Storage::get(static::NEW_VERSION_FILE); + } + + /** + * Delete the version file. + * + * @return bool + */ + protected function deleteVersionFile() : bool + { + return Storage::delete(static::NEW_VERSION_FILE); + } +} diff --git a/src/UpdaterManager.php b/src/UpdaterManager.php index 79b4d6e..480d9a8 100644 --- a/src/UpdaterManager.php +++ b/src/UpdaterManager.php @@ -7,6 +7,7 @@ use Illuminate\Foundation\Application; use Codedge\Updater\Contracts\UpdaterContract; use Codedge\Updater\Contracts\SourceRepositoryTypeContract; +use Codedge\Updater\SourceRepositoryTypes\HttpRepositoryType; use Codedge\Updater\SourceRepositoryTypes\GithubRepositoryType; /** @@ -172,6 +173,20 @@ protected function createGithubRepository(array $config) return $this->sourceRepository(new GithubRepositoryType($client, $config)); } + /** + * Create an instance for the Http source repository. + * + * @param array $config + * + * @return SourceRepository + */ + protected function createHttpRepository(array $config) + { + $client = new Client(); + + return $this->sourceRepository(new HttpRepositoryType($client, $config)); + } + /** * Call a custom source repository type. *