Add Miniflux auto-update feature

This commit is contained in:
Frédéric Guillot 2014-03-30 15:59:26 -04:00
parent d8424ddae9
commit c1221de62c
10 changed files with 310 additions and 25 deletions

View File

@ -10,6 +10,7 @@ require __DIR__.'/models/user.php';
require __DIR__.'/models/feed.php';
require __DIR__.'/models/item.php';
require __DIR__.'/models/schema.php';
require __DIR__.'/models/auto_update.php';
if (file_exists('config.php')) require 'config.php';
@ -24,6 +25,11 @@ defined('PROXY_HOSTNAME') or define('PROXY_HOSTNAME', '');
defined('PROXY_PORT') or define('PROXY_PORT', 3128);
defined('PROXY_USERNAME') or define('PROXY_USERNAME', '');
defined('PROXY_PASSWORD') or define('PROXY_PASSWORD', '');
defined('ROOT_DIRECTORY') or define('ROOT_DIRECTORY', __DIR__);
defined('ENABLE_AUTO_UPDATE') or define('ENABLE_AUTO_UPDATE', true);
defined('AUTO_UPDATE_DOWNLOAD_DIRECTORY') or define('AUTO_UPDATE_DOWNLOAD_DIRECTORY', 'data/download');
defined('AUTO_UPDATE_ARCHIVE_DIRECTORY') or define('AUTO_UPDATE_ARCHIVE_DIRECTORY', 'data/archive');
defined('AUTO_UPDATE_BACKUP_DIRECTORY') or define('AUTO_UPDATE_BACKUP_DIRECTORY', 'data/backup');
PicoFeed\Client::proxy(PROXY_HOSTNAME, PROXY_PORT, PROXY_USERNAME, PROXY_PASSWORD);

View File

@ -7,6 +7,22 @@ use PicoFarad\Session;
use PicoFarad\Template;
use PicoDb\Database;
// Auto-update
Router\get_action('auto-update', function() {
if (ENABLE_AUTO_UPDATE) {
if (Model\AutoUpdate\execute(Model\Config\get('auto_update_url'))) {
Session\flash(t('Miniflux is updated!'));
}
else {
Session\flash_error(t('Unable to update Miniflux, check the console for errors.'));
}
}
Response\redirect('?action=config');
});
// Re-generate tokens
Router\get_action('generate-tokens', function() {

4
data/archive/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

4
data/backup/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

4
data/download/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View File

@ -208,4 +208,9 @@ return array(
'%d months ago' => 'Il y a %d mois',
'Timezone' => 'Fuseau horaire',
'Update all subscriptions' => 'Mettre à jour tous les abonnements',
'Auto-Update URL' => 'URL de mise à jour automatique',
'Update Miniflux' => 'Mettre à jour Miniflux',
'Miniflux is updated!' => 'Miniflux a été mis à jour avec succès !',
'Unable to update Miniflux, check the console for errors.' => 'Impossible de mettre à jour Miniflux, allez-voir les erreurs dans la console.',
'Don\'t forget to backup your database' => 'N\'oubliez pas de sauvegarder votre base de données',
);

229
models/auto_update.php Normal file
View File

@ -0,0 +1,229 @@
<?php
namespace Model\AutoUpdate;
use ZipArchive;
use DirectoryIterator;
use RecursiveIterator;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
// Get all files of a given directory
function get_files_list($directory)
{
$exclude_list = array(
'.git',
'data',
'scripts',
'config.php',
);
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory), RecursiveIteratorIterator::SELF_FIRST);
$files = array();
while ($it->valid()) {
if ($it->isFile() && ! is_excluded_path($it->getSubPathname(), $exclude_list)) {
$files[] = $it->getSubPathname();
}
$it->next();
}
return $files;
}
// Check if the given path is excluded
function is_excluded_path($path, array $exclude_list)
{
foreach ($exclude_list as $excluded_path) {
if (strpos($path, $excluded_path) === 0) {
return true;
}
}
return false;
}
// Synchronize 2 directories (copy/remove files)
function synchronize($source_directory, $destination_directory)
{
\Model\Config\debug('[SYNCHRONIZE] '.$source_directory.' to '.$destination_directory);
$src_files = get_files_list($source_directory);
$dst_files = get_files_list($destination_directory);
// Remove files
$remove_files = array_diff($dst_files, $src_files);
foreach ($remove_files as $file) {
$destination_file = $destination_directory.DIRECTORY_SEPARATOR.$file;
\Model\Config\debug('[REMOVE] '.$destination_file);
if (! @unlink($destination_file)) {
return false;
}
}
// Overwrite all files
foreach ($src_files as $file) {
$directory = $destination_directory.DIRECTORY_SEPARATOR.dirname($file);
if (! is_dir($directory)) {
\Model\Config\debug('[MKDIR] '.$directory);
if (! @mkdir($directory, 0755, true)) {
return false;
}
}
$source_file = $source_directory.DIRECTORY_SEPARATOR.$file;
$destination_file = $destination_directory.DIRECTORY_SEPARATOR.$file;
\Model\Config\debug('[COPY] '.$source_file.' to '.$destination_file);
if (! @copy($source_file, $destination_file)) {
return false;
}
}
return true;
}
// Download and unzip the archive
function uncompress_archive($url, $download_directory = AUTO_UPDATE_DOWNLOAD_DIRECTORY, $archive_directory = AUTO_UPDATE_ARCHIVE_DIRECTORY)
{
$archive_file = $download_directory.DIRECTORY_SEPARATOR.'update.zip';
\Model\Config\debug('[DOWNLOAD] '.$url);
if (($data = @file_get_contents($url)) === false) {
return false;
}
if (@file_put_contents($archive_file, $data) === false) {
return false;
}
\Model\Config\debug('[UNZIP] '.$archive_file);
$zip = new ZipArchive;
if (! $zip->open($archive_file)) {
return false;
}
$zip->extractTo($archive_directory);
$zip->close();
return true;
}
// Remove all files for a given directory
function cleanup_directory($directory)
{
\Model\Config\debug('[CLEANUP] '.$directory);
$dir = new DirectoryIterator($directory);
foreach ($dir as $fileinfo) {
if (! $fileinfo->isDot()) {
$filename = $fileinfo->getRealPath();
if ($fileinfo->isFile()) {
\Model\Config\debug('[REMOVE] '.$filename);
@unlink($filename);
}
else {
cleanup_directory($filename);
@rmdir($filename);
}
}
}
}
// Cleanup all temporary directories
function cleanup_directories()
{
cleanup_directory(AUTO_UPDATE_DOWNLOAD_DIRECTORY);
cleanup_directory(AUTO_UPDATE_ARCHIVE_DIRECTORY);
cleanup_directory(AUTO_UPDATE_BACKUP_DIRECTORY);
}
// Find the archive directory name
function find_archive_root($base_directory = AUTO_UPDATE_ARCHIVE_DIRECTORY)
{
$directory = '';
$dir = new DirectoryIterator($base_directory);
foreach ($dir as $fileinfo) {
if (! $fileinfo->isDot() && $fileinfo->isDir()) {
$directory = $fileinfo->getFilename();
break;
}
}
if (empty($directory)) {
\Model\Config\debug('[FIND ARCHIVE] No directory found');
return false;
}
$path = $base_directory.DIRECTORY_SEPARATOR.$directory;
\Model\Config\debug('[FIND ARCHIVE] '.$path);
return $path;
}
// Check if everything is setup correctly
function check_setup()
{
if (! class_exists('ZipArchive')) die('To use this feature, your PHP installation must be able to uncompress zip files!');
if (AUTO_UPDATE_DOWNLOAD_DIRECTORY === '') die('The constant AUTO_UPDATE_DOWNLOAD_DIRECTORY is not set!');
if (AUTO_UPDATE_ARCHIVE_DIRECTORY === '') die('The constant AUTO_UPDATE_ARCHIVE_DIRECTORY is not set!');
if (AUTO_UPDATE_DOWNLOAD_DIRECTORY === '') die('The constant AUTO_UPDATE_DOWNLOAD_DIRECTORY is not set!');
if (! is_dir(AUTO_UPDATE_DOWNLOAD_DIRECTORY)) @mkdir(AUTO_UPDATE_DOWNLOAD_DIRECTORY, 0755);
if (! is_dir(AUTO_UPDATE_ARCHIVE_DIRECTORY)) @mkdir(AUTO_UPDATE_ARCHIVE_DIRECTORY, 0755);
if (! is_dir(AUTO_UPDATE_BACKUP_DIRECTORY)) @mkdir(AUTO_UPDATE_BACKUP_DIRECTORY, 0755);
if (! is_writable(AUTO_UPDATE_DOWNLOAD_DIRECTORY)) die('Update directories must be writable by your web server user!');
if (! is_writable(__DIR__)) die('Source files must be writable by your web server user!');
}
// Update the source code
function execute($url)
{
check_setup();
cleanup_directories();
if (uncompress_archive($url)) {
$update_directory = find_archive_root();
if ($update_directory) {
// Backup first
if (synchronize(ROOT_DIRECTORY, AUTO_UPDATE_BACKUP_DIRECTORY)) {
// Update
if (synchronize($update_directory, ROOT_DIRECTORY)) {
cleanup_directories();
return true;
}
else {
// If update failed, rollback
synchronize(AUTO_UPDATE_BACKUP_DIRECTORY, ROOT_DIRECTORY);
}
}
}
}
return false;
}

View File

@ -16,10 +16,18 @@ use SimpleValidator\Validator;
use SimpleValidator\Validators;
use PicoDb\Database;
const DB_VERSION = 23;
const DB_VERSION = 24;
const HTTP_USERAGENT = 'Miniflux - http://miniflux.net';
const HTTP_FAKE_USERAGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36';
// Send a debug message to the console
function debug($line)
{
\PicoFeed\Logging::log($line);
write_debug();
}
// Write PicoFeed debug output to a file
function write_debug()
{
@ -211,7 +219,8 @@ function get_all()
'auth_google_token',
'auth_mozilla_token',
'items_sorting_direction',
'redirect_nothing_to_read'
'redirect_nothing_to_read',
'auto_update_url'
)
->findOne();
}
@ -219,32 +228,27 @@ function get_all()
// Validation for edit action
function validate_modification(array $values)
{
$rules = array(
new Validators\Required('username', t('The user name is required')),
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
new Validators\Required('autoflush', t('Value required')),
new Validators\Required('items_per_page', t('Value required')),
new Validators\Integer('items_per_page', t('Must be an integer')),
new Validators\Required('theme', t('Value required')),
);
if (ENABLE_AUTO_UPDATE) {
$rules[] = new Validators\Required('auto_update_url', t('Value required'));
}
if (! empty($values['password'])) {
$v = new Validator($values, array(
new Validators\Required('username', t('The user name is required')),
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
new Validators\Required('password', t('The password is required')),
new Validators\MinLength('password', t('The minimum length is 6 characters'), 6),
new Validators\Required('confirmation', t('The confirmation is required')),
new Validators\Equals('password', 'confirmation', t('Passwords doesn\'t match')),
new Validators\Required('autoflush', t('Value required')),
new Validators\Required('items_per_page', t('Value required')),
new Validators\Integer('items_per_page', t('Must be an integer')),
new Validators\Required('theme', t('Value required')),
));
$rules[] = new Validators\Required('password', t('The password is required'));
$rules[] = new Validators\MinLength('password', t('The minimum length is 6 characters'), 6);
$rules[] = new Validators\Required('confirmation', t('The confirmation is required'));
$rules[] = new Validators\Equals('password', 'confirmation', t('Passwords doesn\'t match'));
}
else {
$v = new Validator($values, array(
new Validators\Required('username', t('The user name is required')),
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
new Validators\Required('autoflush', t('Value required')),
new Validators\Required('items_per_page', t('Value required')),
new Validators\Integer('items_per_page', t('Must be an integer')),
new Validators\Required('theme', t('Value required')),
));
}
$v = new Validator($values, $rules);
return array(
$v->execute(),

View File

@ -2,6 +2,11 @@
namespace Schema;
function version_24($pdo)
{
$pdo->exec("ALTER TABLE config ADD COLUMN auto_update_url TEXT DEFAULT 'https://github.com/fguillot/miniflux/archive/master.zip'");
}
function version_23($pdo)
{

View File

@ -36,6 +36,11 @@
<?= Helper\form_checkbox('nocontent', t('Do not fetch the content of articles'), 1, isset($values['nocontent']) ? $values['nocontent'] : false) ?><br />
<?php if (ENABLE_AUTO_UPDATE): ?>
<?= Helper\form_label(t('Auto-Update URL'), 'auto_update_url') ?>
<?= Helper\form_text('auto_update_url', $values, $errors, array('required')) ?><br/>
<?php endif ?>
<ul>
<li>
<?php if ($values['auth_google_token']): ?>
@ -93,6 +98,9 @@
<ul>
<li><?= t('Miniflux version:') ?> <strong><?= APP_VERSION ?></strong></li>
<li><?= t('Official website:') ?> <a href="http://miniflux.net" rel="noreferer" target="_blank">http://miniflux.net</a></li>
<?php if (ENABLE_AUTO_UPDATE): ?>
<li><a href="?action=auto-update"><?= t('Update Miniflux') ?></a> (<?= t('Don\'t forget to backup your database') ?>)</li>
<?php endif ?>
<li><a href="?action=console"><?= t('Console') ?></a></li>
</ul>
</div>